{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "run-timeline",
  "type": "registry:component",
  "title": "Run Timeline",
  "description": "Multi-lane execution timeline showing run phases over time, with optional cursor.",
  "dependencies": [
    "@vllnt/ui@^0.2.1"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/run-timeline/run-timeline.tsx",
      "content": "\"use client\";\n\nimport {\n  type ComponentPropsWithoutRef,\n  forwardRef,\n  type ReactNode,\n} from \"react\";\n\nimport { cn } from \"@vllnt/ui\";\n\n/**\n * Phase state — drives the bar tone.\n *\n * @public\n */\nexport type RunPhaseState =\n  | \"complete\"\n  | \"failed\"\n  | \"queued\"\n  | \"running\"\n  | \"stopped\";\n\nconst STATE_FILL: Record<RunPhaseState, string> = {\n  complete: \"bg-emerald-500/70\",\n  failed: \"bg-red-500/70\",\n  queued: \"bg-amber-500/70\",\n  running: \"bg-blue-500/70\",\n  stopped: \"bg-muted-foreground/40\",\n};\n\nconst STATE_LABEL: Record<RunPhaseState, string> = {\n  complete: \"Complete\",\n  failed: \"Failed\",\n  queued: \"Queued\",\n  running: \"Running\",\n  stopped: \"Stopped\",\n};\n\n/**\n * One lane in a multi-lane run timeline.\n *\n * @public\n */\nexport type RunTimelineLane = {\n  /** Stable identifier — used as the React key + phase routing. */\n  id: string;\n  /** Display label rendered to the left of the lane. */\n  label: ReactNode;\n};\n\n/**\n * One phase block in the run timeline.\n *\n * @public\n */\nexport type RunTimelinePhase = {\n  /** End timestamp in the same units as `start` / `end`. */\n  end: number;\n  /** Stable identifier — used as the React key + analytics hook. */\n  id: string;\n  /** Optional label rendered inside the phase bar. */\n  label?: ReactNode;\n  /** Lane id this phase belongs to. Defaults to `\"default\"` (single-lane mode). */\n  laneId?: string;\n  /** Optional click handler — when provided, the phase becomes a button. */\n  onActivate?: () => void;\n  /** Start timestamp `>= timeline start`. */\n  start: number;\n  /** Phase state. Defaults to `\"running\"`. */\n  state?: RunPhaseState;\n};\n\n/**\n * Localizable strings.\n *\n * @public\n */\nexport type RunTimelineLabels = {\n  /** Empty-state copy. Defaults to `\"No phases\"`. */\n  empty?: string;\n  /** Aria-label override. Defaults to `\"Run timeline\"`. */\n  region?: string;\n};\n\nconst DEFAULT_LABELS = {\n  empty: \"No phases\",\n  region: \"Run timeline\",\n} as const satisfies Required<RunTimelineLabels>;\n\n/**\n * Props for {@link RunTimeline}.\n *\n * @public\n */\nexport type RunTimelineProps = {\n  /** Optional cursor position in the same units as the range. Renders a vertical line. */\n  cursor?: number;\n  /** End of the time range. Must be `> start`. */\n  end: number;\n  /** Optional formatter for the start / cursor / end labels. */\n  formatValue?: (value: number) => ReactNode;\n  /** Localizable strings. */\n  labels?: RunTimelineLabels;\n  /** Optional explicit lane definitions in render order. Required for multi-lane mode. */\n  lanes?: RunTimelineLane[];\n  /** Phase blocks — order is irrelevant; routed to lanes by `laneId`. */\n  phases: RunTimelinePhase[];\n  /** Start of the time range. */\n  start: number;\n} & ComponentPropsWithoutRef<\"section\">;\n\nconst clamp = (v: number, min: number, max: number): number => {\n  if (v < min) {\n    return min;\n  }\n  if (v > max) {\n    return max;\n  }\n  return v;\n};\n\nconst Endpoints = (props: {\n  cursor?: number;\n  end: number;\n  format?: (v: number) => ReactNode;\n  start: number;\n}): React.ReactElement => {\n  const fmt = props.format;\n  const showCursor = typeof props.cursor === \"number\";\n  return (\n    <div className=\"flex items-baseline justify-between gap-2 text-[11px] text-muted-foreground\">\n      <span data-run-timeline-start>\n        {fmt ? fmt(props.start) : props.start}\n      </span>\n      {showCursor ? (\n        <span\n          className=\"font-semibold text-foreground\"\n          data-run-timeline-cursor-label\n        >\n          {fmt ? fmt(props.cursor ?? 0) : props.cursor}\n        </span>\n      ) : null}\n      <span data-run-timeline-end>{fmt ? fmt(props.end) : props.end}</span>\n    </div>\n  );\n};\n\nconst PhaseBar = (props: {\n  laneIndex: number;\n  laneTotal: number;\n  phase: RunTimelinePhase;\n  span: number;\n  start: number;\n}): React.ReactElement => {\n  const { laneIndex, laneTotal, phase, span, start } = props;\n  const left = clamp((phase.start - start) / span, 0, 1) * 100;\n  const right = clamp((phase.end - start) / span, 0, 1) * 100;\n  const width = Math.max(right - left, 0.5);\n  const top = (laneIndex / laneTotal) * 100;\n  const height = 100 / laneTotal;\n  const state = phase.state ?? \"running\";\n  const sharedStyle = {\n    height: `${height}%`,\n    left: `${left}%`,\n    top: `${top}%`,\n    width: `${width}%`,\n  };\n  const ariaLabel = `${STATE_LABEL[state]} ${phase.start} → ${phase.end}`;\n  if (phase.onActivate) {\n    const handleClick = (): void => {\n      phase.onActivate?.();\n    };\n    return (\n      <button\n        aria-label={ariaLabel}\n        className={cn(\n          \"absolute flex items-center justify-start overflow-hidden truncate rounded-sm border border-border/50 px-1 text-left text-[10px] text-foreground transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n          STATE_FILL[state],\n        )}\n        data-run-phase={phase.id}\n        data-run-phase-state={state}\n        onClick={handleClick}\n        style={sharedStyle}\n        type=\"button\"\n      >\n        {phase.label}\n      </button>\n    );\n  }\n  return (\n    <span\n      aria-label={ariaLabel}\n      className={cn(\n        \"absolute flex items-center justify-start overflow-hidden truncate rounded-sm border border-border/50 px-1 text-[10px] text-foreground\",\n        STATE_FILL[state],\n      )}\n      data-run-phase={phase.id}\n      data-run-phase-state={state}\n      role=\"img\"\n      style={sharedStyle}\n    >\n      {phase.label}\n    </span>\n  );\n};\n\ntype LaneViewInput = {\n  cursor?: number;\n  end: number;\n  lanes: RunTimelineLane[];\n  phases: RunTimelinePhase[];\n  start: number;\n};\n\nconst TrackBody = (props: LaneViewInput): React.ReactElement => {\n  const { cursor, end, lanes, phases, start } = props;\n  const span = end - start;\n  const cursorRatio =\n    typeof cursor === \"number\" ? clamp((cursor - start) / span, 0, 1) : null;\n  return (\n    <div className=\"flex items-stretch\">\n      <div className=\"flex w-24 flex-col gap-px text-[10px] uppercase tracking-wide text-muted-foreground\">\n        {lanes.map((lane) => (\n          <div\n            className=\"flex h-7 items-center px-2\"\n            data-run-timeline-lane={lane.id}\n            key={lane.id}\n          >\n            {lane.label}\n          </div>\n        ))}\n      </div>\n      <div\n        className=\"relative flex-1 overflow-hidden rounded-md border border-border bg-muted/20\"\n        data-run-timeline-track\n        style={{ height: `${lanes.length * 28}px` }}\n      >\n        {phases.map((phase) => {\n          const index = lanes.findIndex(\n            (lane) => lane.id === (phase.laneId ?? \"default\"),\n          );\n          if (index === -1) {\n            return null;\n          }\n          return (\n            <PhaseBar\n              key={phase.id}\n              laneIndex={index}\n              laneTotal={lanes.length}\n              phase={phase}\n              span={span}\n              start={start}\n            />\n          );\n        })}\n        {cursorRatio === null ? null : (\n          <span\n            aria-hidden=\"true\"\n            className=\"absolute top-0 bottom-0 w-px bg-foreground\"\n            data-run-timeline-cursor\n            style={{ left: `${cursorRatio * 100}%` }}\n          />\n        )}\n      </div>\n    </div>\n  );\n};\n\nconst resolveLanes = (\n  explicit: RunTimelineLane[] | undefined,\n): RunTimelineLane[] => {\n  if (explicit && explicit.length > 0) {\n    return explicit;\n  }\n  return [{ id: \"default\", label: \"Run\" }];\n};\n\n/**\n * Multi-lane execution timeline showing run phases over time. Each\n * phase renders as a colored bar positioned by its start / end and\n * routed to a lane by `laneId`. Optional cursor draws a thin vertical\n * line for the current playback position.\n *\n * Pure presentation; the host computes the phase list from the run's\n * execution history. Pair with {@link \"../timeline-scrubber/timeline-scrubber\".TimelineScrubber}\n * to drive the cursor.\n *\n * Distinct from `MapTimeline` (geo-aware), `Stepper` (sequential\n * steps), and the timeline family `#32`–`#35`: this primitive is\n * specifically the run history surface inside the canvas.\n *\n * @example\n * ```tsx\n * <RunTimeline\n *   start={0} end={3600}\n *   cursor={1800}\n *   lanes={[\n *     { id: \"ingest\", label: \"Ingest\" },\n *     { id: \"rank\",   label: \"Rank\" },\n *   ]}\n *   phases={[\n *     { id: \"1\", laneId: \"ingest\", start: 0,    end: 600,  state: \"complete\", label: \"load\" },\n *     { id: \"2\", laneId: \"rank\",   start: 600,  end: 2400, state: \"running\",  label: \"score\" },\n *   ]}\n * />\n * ```\n *\n * @public\n */\nexport const RunTimeline = forwardRef<HTMLElement, RunTimelineProps>(\n  (props, ref) => {\n    const {\n      className,\n      cursor,\n      end,\n      formatValue,\n      labels,\n      lanes,\n      phases,\n      start,\n      ...rest\n    } = props;\n    const resolvedLabels = { ...DEFAULT_LABELS, ...labels };\n    const safeEnd = end <= start ? start + 1 : end;\n    const resolvedLanes = resolveLanes(lanes);\n    return (\n      <section\n        aria-label={resolvedLabels.region}\n        className={cn(\"flex w-full flex-col gap-2\", className)}\n        data-run-timeline\n        ref={ref}\n        {...rest}\n      >\n        <Endpoints\n          cursor={cursor}\n          end={safeEnd}\n          format={formatValue}\n          start={start}\n        />\n        {phases.length === 0 ? (\n          <p\n            className=\"rounded-md border border-border bg-muted/20 px-2 py-3 text-center text-[11px] text-muted-foreground\"\n            data-run-timeline-state=\"empty\"\n          >\n            {resolvedLabels.empty}\n          </p>\n        ) : (\n          <TrackBody\n            cursor={cursor}\n            end={safeEnd}\n            lanes={resolvedLanes}\n            phases={phases}\n            start={start}\n          />\n        )}\n      </section>\n    );\n  },\n);\nRunTimeline.displayName = \"RunTimeline\";\n",
      "type": "registry:component"
    }
  ],
  "version": "0.2.1",
  "stability": "stable"
}
