{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "run-timeline",
  "title": "RunTimeline",
  "description": "The run's steps and tool calls. It follows the newest step until you scroll up.",
  "registryDependencies": [
    "https://creance.pages.dev/r/button.json",
    "https://creance.pages.dev/r/code.json",
    "https://creance.pages.dev/r/creance-base.json",
    "https://creance.pages.dev/r/status-badge.json",
    "https://creance.pages.dev/r/tool-call-header.json"
  ],
  "files": [
    {
      "path": "run-timeline/RunTimeline.css",
      "content": ".cr-timeline {\n  position: relative;\n  display: flex;\n  flex-direction: column;\n  min-height: 0;\n  height: 100%;\n  color: var(--cr-color-text-default);\n  font-family: var(--cr-font-family-sans);\n  font-size: var(--cr-font-size-md);\n}\n\n.cr-timeline__scroll {\n  flex: 1;\n  min-height: 0;\n  overflow-y: auto;\n  padding: var(--cr-space-3);\n  overscroll-behavior: contain;\n}\n\n.cr-timeline__scroll:focus-visible {\n  outline: var(--cr-focus-width) solid var(--cr-color-focus);\n  outline-offset: calc(-1 * var(--cr-focus-width));\n}\n\n.cr-timeline__list {\n  display: grid;\n  gap: var(--cr-space-1);\n  margin: 0;\n  padding: 0;\n  list-style: none;\n}\n\n.cr-timeline__list .cr-timeline__list {\n  margin-top: var(--cr-space-2);\n}\n\n.cr-timeline__step {\n  position: relative;\n  display: grid;\n  grid-template-columns: 20px 1fr;\n  gap: var(--cr-space-2);\n  padding-bottom: var(--cr-space-3);\n}\n\n/* The rail connecting markers. */\n.cr-timeline__step:not(:last-child)::before {\n  content: '';\n  position: absolute;\n  top: 22px;\n  bottom: 0;\n  left: 9px;\n  width: 2px;\n  background: var(--cr-color-border-default);\n}\n\n.cr-timeline__marker {\n  display: grid;\n  place-items: center;\n  width: 20px;\n  height: 20px;\n  margin-top: 1px;\n  border-radius: var(--cr-radius-full);\n  box-shadow: inset 0 0 0 1px color-mix(in srgb, currentColor 25%, transparent);\n  font-size: var(--cr-font-size-xs);\n}\n\n.cr-timeline__step[data-status='running'] > .cr-timeline__marker { background: var(--cr-color-status-running-bg); color: var(--cr-color-status-running-fg); }\n.cr-timeline__step[data-status='waiting'] > .cr-timeline__marker { background: var(--cr-color-status-waiting-bg); color: var(--cr-color-status-waiting-fg); }\n.cr-timeline__step[data-status='done'] > .cr-timeline__marker    { background: var(--cr-color-status-done-bg);    color: var(--cr-color-status-done-fg); }\n.cr-timeline__step[data-status='failed'] > .cr-timeline__marker  { background: var(--cr-color-status-failed-bg);  color: var(--cr-color-status-failed-fg); }\n\n.cr-timeline__body {\n  min-width: 0;\n}\n\n.cr-timeline__row {\n  display: flex;\n  flex-wrap: wrap;\n  align-items: center;\n  justify-content: space-between;\n  gap: var(--cr-space-2);\n  min-height: 22px;\n}\n\n.cr-timeline__title {\n  font-weight: var(--cr-font-weight-medium);\n}\n\n.cr-timeline__calls {\n  display: grid;\n  gap: var(--cr-space-1);\n  margin: var(--cr-space-2) 0 0;\n  padding: 0;\n  list-style: none;\n}\n\n.cr-timeline__call-row,\n.cr-timeline__call-button {\n  box-sizing: border-box;\n  display: flex;\n  flex-wrap: wrap;\n  align-items: center;\n  gap: var(--cr-space-2);\n  width: 100%;\n  padding: 6px var(--cr-space-3);\n  border: 1px solid transparent;\n  border-radius: var(--cr-radius-md);\n  background: var(--cr-color-glass-tile);\n  color: inherit;\n  font: inherit;\n  font-size: var(--cr-font-size-sm);\n  text-align: start;\n}\n\n.cr-timeline__call-row .cr-badge,\n.cr-timeline__call-button .cr-badge {\n  margin-inline-start: auto;\n}\n\n.cr-timeline__call-button {\n  cursor: pointer;\n}\n\n.cr-timeline__call-button:hover {\n  border-color: var(--cr-color-border-strong);\n}\n\n.cr-timeline__call-button:focus-visible {\n  outline: var(--cr-focus-width) solid var(--cr-color-focus);\n  outline-offset: var(--cr-focus-offset);\n}\n\n.cr-timeline__call-button[aria-current='true'] {\n  border-color: var(--cr-color-ink-bg);\n  background: var(--cr-color-bg-raised);\n}\n\n/* A red edge, not a red fill: the failed badge inside already carries the tint. */\n.cr-timeline__call[data-status='error'] > :first-child {\n  border-color: color-mix(in srgb, var(--cr-color-status-failed-fg) 50%, transparent);\n}\n\n.cr-timeline__attempt {\n  color: var(--cr-color-text-muted);\n  font-size: var(--cr-font-size-xs);\n}\n\n.cr-timeline__error {\n  margin: var(--cr-space-1) 0 0;\n  color: var(--cr-color-status-failed-fg);\n  font-size: var(--cr-font-size-sm);\n  overflow-wrap: anywhere;\n}\n\n.cr-timeline__empty {\n  margin: 0;\n  color: var(--cr-color-text-muted);\n}\n\n.cr-timeline__jump {\n  position: absolute;\n  z-index: 1;\n  bottom: var(--cr-space-3);\n  left: 50%;\n  transform: translateX(-50%);\n  box-shadow: var(--cr-shadow-md);\n}\n",
      "type": "registry:file",
      "target": "@components/creance/run-timeline/RunTimeline.css"
    },
    {
      "path": "run-timeline/RunTimeline.tsx",
      "content": "'use client';\n\nimport { useEffect, useLayoutEffect, useRef, useState } from 'react';\nimport { Button } from '@/components/creance/button/Button';\nimport { Code } from '@/components/creance/code/Code';\nimport { StatusBadge } from '@/components/creance/status-badge/StatusBadge';\nimport type { ToolCallStatus } from '@/components/creance/tool-call-header/ToolCallHeader';\nimport { cx } from '@/lib/creance/cx';\nimport { ArrowDownIcon, CheckIcon, CrossIcon, DotIcon, PauseIcon } from '@/lib/creance/icons';\nimport { LiveRegion } from '@/lib/creance/LiveRegion';\nimport './RunTimeline.css';\n\nexport type TimelineStepStatus = 'running' | 'waiting' | 'done' | 'failed';\n\nexport interface TimelineToolCall {\n  id: string;\n  tool: string;\n  status: ToolCallStatus;\n  startedAt?: number | undefined;\n  endedAt?: number | undefined;\n  attempt?: number | undefined;\n  error?: string | undefined;\n}\n\nexport interface TimelineStep {\n  id: string;\n  title: string;\n  status: TimelineStepStatus;\n  startedAt?: number | undefined;\n  endedAt?: number | undefined;\n  error?: string | undefined;\n  toolCalls?: TimelineToolCall[];\n  children?: TimelineStep[];\n}\n\nexport interface RunTimelineProps {\n  steps: TimelineStep[];\n  /** Follow new steps as they stream in. Following pauses when the person scrolls up. Default true. */\n  live?: boolean;\n  /** Accessible name of the scrollable region. */\n  label?: string;\n  /** Makes tool calls selectable, e.g. to show a ToolCallCard in a detail panel. */\n  onSelectToolCall?: (id: string) => void;\n  selectedToolCallId?: string | undefined;\n  /** Shown before the first step arrives. */\n  emptyText?: string;\n  className?: string;\n}\n\nconst stepIcon = { running: <DotIcon />, waiting: <PauseIcon />, done: <CheckIcon />, failed: <CrossIcon /> };\nconst badge = {\n  running: { status: 'running', label: 'Running' },\n  waiting: { status: 'waiting', label: 'Waiting for you' },\n  done: { status: 'done', label: 'Done' },\n  failed: { status: 'failed', label: 'Failed' },\n} as const;\nconst callBadge = {\n  pending: { status: 'waiting', label: 'Pending' },\n  running: { status: 'running', label: 'Running' },\n  success: { status: 'done', label: 'Succeeded' },\n  error: { status: 'failed', label: 'Failed' },\n} as const;\n\n/** Distance from the bottom, in px, that still counts as \"at the latest\". */\nconst PIN_THRESHOLD = 32;\n\nfunction walk(steps: TimelineStep[], visit: (s: TimelineStep) => void) {\n  for (const s of steps) {\n    visit(s);\n    if (s.children) walk(s.children, visit);\n  }\n}\n\n/** The newest thing worth announcing: a failure or a step that needs the person. */\nfunction announcementFor(steps: TimelineStep[], seen: Set<string>): string | undefined {\n  let message: string | undefined;\n  walk(steps, (s) => {\n    const key = `${s.id}:${s.status}`;\n    if ((s.status === 'failed' || s.status === 'waiting') && !seen.has(key)) {\n      seen.add(key);\n      message = s.status === 'failed' ? `Step failed: ${s.title}. ${s.error ?? ''}` : `Waiting for you: ${s.title}`;\n    }\n    for (const c of s.toolCalls ?? []) {\n      const callKey = `${c.id}:${c.attempt ?? 1}:${c.status}`;\n      if (c.status === 'error' && !seen.has(callKey)) {\n        seen.add(callKey);\n        message = `Tool call ${c.tool} failed: ${c.error ?? ''}`;\n      }\n    }\n  });\n  return message;\n}\n\nexport function RunTimeline({\n  steps,\n  live = true,\n  label = 'Run timeline',\n  onSelectToolCall,\n  selectedToolCallId,\n  emptyText = 'Waiting for the agent to start.',\n  className,\n}: RunTimelineProps) {\n  const scrollRef = useRef<HTMLDivElement>(null);\n  const pinned = useRef(true);\n  const jumping = useRef(false);\n  const [hasNew, setHasNew] = useState(false);\n  const seen = useRef(new Set<string>());\n  const [announcement, setAnnouncement] = useState('');\n\n  useEffect(() => {\n    const message = announcementFor(steps, seen.current);\n    if (message) setAnnouncement(message);\n  }, [steps]);\n\n  // New content: follow it if the person is at the bottom, otherwise offer \"Jump to latest\".\n  useLayoutEffect(() => {\n    const el = scrollRef.current;\n    if (!el || !live) return;\n    if (pinned.current) el.scrollTop = el.scrollHeight;\n    else if (el.scrollHeight > el.clientHeight) setHasNew(true);\n  }, [steps, live]);\n\n  function onScroll() {\n    const el = scrollRef.current;\n    if (!el) return;\n    const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < PIN_THRESHOLD;\n    if (jumping.current) {\n      if (atBottom) jumping.current = false;\n      return;\n    }\n    pinned.current = atBottom;\n    if (atBottom) setHasNew(false);\n  }\n\n  function jumpToLatest() {\n    const el = scrollRef.current;\n    if (!el) return;\n    const reduce = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;\n    pinned.current = true;\n    jumping.current = !reduce;\n    setHasNew(false);\n    el.scrollTo({ top: el.scrollHeight, behavior: reduce ? 'auto' : 'smooth' });\n  }\n\n  function renderSteps(list: TimelineStep[]) {\n    return (\n      <ol className=\"cr-timeline__list\">\n        {list.map((step) => (\n          <li key={step.id} className=\"cr-timeline__step\" data-status={step.status}>\n            <span className=\"cr-timeline__marker\" aria-hidden=\"true\">\n              {stepIcon[step.status]}\n            </span>\n            <div className=\"cr-timeline__body\">\n              <div className=\"cr-timeline__row\">\n                <span className=\"cr-timeline__title\">{step.title}</span>\n                <StatusBadge\n                  status={badge[step.status].status}\n                  label={badge[step.status].label}\n                  startedAt={step.status === 'waiting' ? undefined : step.startedAt}\n                  endedAt={step.endedAt}\n                />\n              </div>\n              {step.error && <p className=\"cr-timeline__error\">{step.error}</p>}\n              {!!step.toolCalls?.length && (\n                <ul className=\"cr-timeline__calls\" aria-label={`Tool calls in ${step.title}`}>\n                  {step.toolCalls.map((call) => {\n                    const content = (\n                      <>\n                        <Code>{call.tool}</Code>\n                        {(call.attempt ?? 1) > 1 && <span className=\"cr-timeline__attempt\">attempt {call.attempt}</span>}\n                        <StatusBadge\n                          status={callBadge[call.status].status}\n                          label={callBadge[call.status].label}\n                          startedAt={call.status === 'pending' ? undefined : call.startedAt}\n                          endedAt={call.endedAt}\n                        />\n                      </>\n                    );\n                    return (\n                      <li key={call.id} className=\"cr-timeline__call\" data-status={call.status}>\n                        {onSelectToolCall ? (\n                          <button\n                            type=\"button\"\n                            className=\"cr-timeline__call-button\"\n                            aria-current={selectedToolCallId === call.id ? 'true' : undefined}\n                            onClick={() => onSelectToolCall(call.id)}\n                          >\n                            {content}\n                          </button>\n                        ) : (\n                          <div className=\"cr-timeline__call-row\">{content}</div>\n                        )}\n                        {call.status === 'error' && call.error && (\n                          <p className=\"cr-timeline__error\">{call.error}</p>\n                        )}\n                      </li>\n                    );\n                  })}\n                </ul>\n              )}\n              {!!step.children?.length && renderSteps(step.children)}\n            </div>\n          </li>\n        ))}\n      </ol>\n    );\n  }\n\n  return (\n    <div className={cx('cr-timeline', className)}>\n      {/* Focusable so keyboard users can scroll it. */}\n      {/* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */}\n      <div ref={scrollRef} className=\"cr-timeline__scroll\" role=\"region\" aria-label={label} tabIndex={0} onScroll={onScroll}>\n        {steps.length ? renderSteps(steps) : <p className=\"cr-timeline__empty\">{emptyText}</p>}\n      </div>\n      {hasNew && (\n        <Button size=\"sm\" variant=\"primary\" className=\"cr-timeline__jump\" onClick={jumpToLatest}>\n          <ArrowDownIcon /> Jump to latest\n        </Button>\n      )}\n      <LiveRegion message={announcement} />\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@components/creance/run-timeline/RunTimeline.tsx"
    }
  ],
  "categories": [
    "organism",
    "agent"
  ],
  "type": "registry:ui"
}