{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"run-trace","type":"registry:component","title":"Run trace","description":"One agent run as a waterfall: every step (model calls, tools, retrieval, checks, sub-agents) nested under the step that started it, each a bar on a shared time axis, with retries as separate attempts on the same bar, tokens and cost per step and for the run; while it runs the bars grow live, and choosing a step shows its timing, attempts, input and output beside the trace.","categories":["ai"],"dependencies":["@radix-ui/react-direction@1.1.4","clsx@^2.1.1","lucide-react@^1.48.0","tailwind-merge@^3.7.0"],"registryDependencies":["https://beamline.io/registry/look-prism.json"],"files":[{"path":"beamline/components/run-trace/messages.ts","type":"registry:file","target":"src/ui/beamline/components/run-trace/messages.ts","content":"import type { MessageDefinitions } from \"../../runtime/messages\";\n/** Built-in English copy; stable keys are the public translation API. */\nexport const english = {\n  running: \"Running\",\n  failed: \"Failed\",\n  succeeded: \"Succeeded\",\n  time: \"Time\",\n  steps: \"Steps\",\n  failed2: \"· {errors} failed\",\n  tokens: \"Tokens\",\n  inOut: \"{value} in · {value2} out\",\n  notReported: \"Not reported\",\n  cost: \"Cost\",\n  noStepsYetTheyAppearHereAs: \"No steps yet. They appear here as the run starts them.\",\n  trace: \"Trace: {title}\",\n  runTrace: \"Run trace\",\n  close: \"Close {name}\",\n  open: \"Open {name}\",\n  failed3: \"failed\",\n  running2: \"running\",\n  cancelled: \"cancelled\",\n  done: \"done\",\n  attempts: \", {value} attempts\",\n  details: \"{name} details\",\n  kind: \"Kind\",\n  started: \"Started\",\n  at: \"at {value}\",\n  took: \"Took\",\n  soFar: \" so far\",\n  attempts2: \"Attempts\",\n  after: \"after {value}\",\n  input: \"Input\",\n  error: \"Error\",\n  output: \"Output\",\n  agent: \"Agent\",\n  model: \"Model\",\n  tool: \"Tool\",\n  retrieval: \"Retrieval\",\n  check: \"Check\",\n  wait: \"Wait\",\n  /** A ghost re-run under the real run. */\n  ghostSays: \"ghost {value}\",\n  same: \"same\",\n  notRun: \"not run\",\n  ghostOnly: \"ghost only\",\n  queued: \"not started\",\n  noDifference: \"no difference\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/run-trace/run-trace.module.css","type":"registry:file","target":"src/ui/beamline/components/run-trace/run-trace.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  .root {\n    container-type: inline-size;\n    display: grid;\n    min-width: min(18rem, 100%);\n    overflow: hidden;\n    border-radius: var(--pui-radius-card);\n    --_name: minmax(10rem, 34%);\n    --_dur: 4.5rem;\n    --_row: 1.875rem;\n  }\n  .root[data-density=\"default\"] {\n    --_row: 2.25rem;\n  }\n  .root[data-room=\"narrow\"] {\n    --_name: minmax(8rem, 46%);\n    --_dur: 3.75rem;\n  }\n  /* with a ghost the last column carries each step's difference under its time */\n  .root[data-ghosted] {\n    --_dur: 7.5rem;\n  }\n  .root[data-ghosted][data-room=\"narrow\"] {\n    --_dur: 5.5rem;\n  }\n  .root[data-ghosted] .duration {\n    flex-direction: column;\n    align-items: flex-end;\n    justify-content: center;\n    gap: 0;\n    line-height: 1.2;\n  }\n\n  .summary {\n    display: flex;\n    flex-wrap: wrap;\n    align-items: center;\n    gap: var(--pui-space-2) var(--pui-space-4);\n    padding: var(--pui-space-3) var(--pui-space-4);\n    border-bottom: 1px solid var(--pui-line-soft);\n  }\n  .runStatus {\n    display: inline-flex;\n    align-items: center;\n    gap: var(--pui-space-1-5);\n    color: var(--pui-ink);\n    font-size: var(--pui-text-sm);\n    font-weight: var(--pui-weight-medium);\n  }\n  .runStatus :where(svg) {\n    width: var(--pui-icon-md);\n    height: var(--pui-icon-md);\n  }\n  .runStatus[data-status=\"ok\"] :where(svg) {\n    color: var(--pui-success);\n  }\n  .runStatus[data-status=\"error\"] :where(svg) {\n    color: var(--pui-danger);\n  }\n  .title {\n    min-width: 0;\n    flex: 1;\n    overflow: hidden;\n    color: var(--pui-ink-2);\n    font-size: var(--pui-text-sm);\n    text-overflow: ellipsis;\n    white-space: nowrap;\n  }\n  .facts {\n    display: flex;\n    flex-wrap: wrap;\n    gap: var(--pui-space-1) var(--pui-space-4);\n    margin-inline-start: auto;\n    font-size: var(--pui-text-xs);\n  }\n  .facts > div {\n    display: flex;\n    gap: var(--pui-space-1-5);\n  }\n  .facts dt {\n    color: var(--pui-ink-3);\n  }\n  .facts dd {\n    color: var(--pui-ink);\n  }\n  .errCount {\n    color: var(--pui-danger);\n  }\n\n  .body {\n    display: grid;\n    min-height: 0;\n  }\n  @container (min-width: 54rem) {\n    .body:has(> .detail) {\n      grid-template-columns: minmax(0, 1fr) 18rem;\n    }\n  }\n  .trace {\n    min-width: 0;\n    padding: var(--pui-space-2) var(--pui-space-2) var(--pui-space-3);\n  }\n\n  /* the axis and every row share one template: name, lane, duration */\n  .axis,\n  .row {\n    display: grid;\n    grid-template-columns: var(--_name) minmax(0, 1fr) var(--_dur);\n    align-items: center;\n    gap: var(--pui-space-2);\n  }\n  .axis {\n    height: 1.5rem;\n    padding-inline: var(--pui-space-2);\n    color: var(--pui-ink-3);\n    font-size: var(--pui-text-2xs);\n    font-variant-numeric: tabular-nums;\n  }\n  .lane {\n    position: relative;\n    height: 100%;\n    min-width: 0;\n    /* marks are lane-wide boxes moved by transforms: nothing they carry past the lane may scroll the page */\n    overflow-x: clip;\n  }\n  .axis .lane {\n    border-bottom: 1px solid var(--pui-line-soft);\n  }\n  .tick {\n    position: absolute;\n    bottom: 3px;\n    translate: -50% 0;\n    white-space: nowrap;\n  }\n  .tick:first-child {\n    translate: 0 0;\n  }\n  /* now, while the run is live: one line through the axis and every row, on the rows' own template */\n  .nowOverlay {\n    position: absolute;\n    inset: 0;\n    z-index: 0;\n    display: grid;\n    grid-template-columns: var(--_name) minmax(0, 1fr) var(--_dur);\n    gap: var(--pui-space-2);\n    padding-inline: var(--pui-space-2);\n    pointer-events: none;\n  }\n  /* the now line: a lane-wide box with its start edge drawn, moved by a transform */\n  .nowLine {\n    position: absolute;\n    top: 0;\n    bottom: 0;\n    inset-inline-start: 0;\n    width: 100%;\n    border-inline-start: 1px solid var(--pui-accent);\n    transform: translateX(calc(var(--s) * 100%));\n    transition: transform var(--pui-duration-fast) linear;\n  }\n  .nowLine:dir(rtl) {\n    transform: translateX(calc(var(--s) * -100%));\n  }\n  .empty {\n    padding: var(--pui-space-6) var(--pui-space-3);\n    color: var(--pui-ink-3);\n    font-size: var(--pui-text-sm);\n    text-align: center;\n  }\n\n  .rows {\n    position: relative;\n    display: grid;\n  }\n  .row {\n    position: relative;\n    min-height: var(--_row);\n    padding-inline: var(--pui-space-2);\n    border-radius: var(--pui-radius-chip);\n    cursor: pointer;\n    outline-offset: -2px;\n  }\n  .row:hover {\n    background-color: var(--pui-hover);\n  }\n  .row[aria-selected=\"true\"] {\n    background-color: var(--pui-current-tint, var(--pui-accent-tint));\n    box-shadow: inset 2px 0 0 var(--pui-current-edge, var(--pui-accent));\n  }\n  .row[aria-selected=\"true\"]:dir(rtl) {\n    box-shadow: inset -2px 0 0 var(--pui-current-edge, var(--pui-accent));\n  }\n  .name {\n    display: flex;\n    min-width: 0;\n    align-items: center;\n    gap: var(--pui-space-1);\n    color: var(--pui-ink);\n    font-size: var(--pui-text-xs);\n  }\n  .indent {\n    width: calc(var(--_depth, 0) * 0.875rem);\n    flex-shrink: 0;\n  }\n  .chevron,\n  .chevronSpace {\n    display: grid;\n    width: 1rem;\n    height: 1rem;\n    flex-shrink: 0;\n    place-items: center;\n    border-radius: 3px;\n    color: var(--pui-ink-3);\n  }\n  .chevron:hover {\n    background: var(--pui-hover-2);\n    color: var(--pui-ink);\n  }\n  .chevron :where(svg) {\n    width: var(--pui-icon-sm);\n    height: var(--pui-icon-sm);\n    transition: rotate var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .chevron[data-open] :where(svg) {\n    rotate: 90deg;\n  }\n  .chevron:dir(rtl) :where(svg) {\n    scale: -1 1;\n  }\n  .kindIcon {\n    display: grid;\n    flex-shrink: 0;\n    place-items: center;\n    color: var(--_kind, var(--pui-ink-3));\n  }\n  .kindIcon :where(svg) {\n    width: 0.8125rem;\n    height: 0.8125rem;\n  }\n  .label {\n    min-width: 0;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n  }\n  .model {\n    flex-shrink: 1;\n    min-width: 0;\n    overflow: hidden;\n    color: var(--pui-ink-3);\n    font-family: var(--pui-font-code);\n    font-size: var(--pui-text-2xs);\n    text-overflow: ellipsis;\n    white-space: nowrap;\n  }\n  .root[data-room=\"narrow\"] .model {\n    display: none;\n  }\n\n  /* kinds take series colours; the bar is the step's time on the run's axis */\n  .row[data-kind=\"llm\"],\n  .detailBody {\n    --_kind: var(--pui-series-1);\n  }\n  .row[data-kind=\"tool\"] {\n    --_kind: var(--pui-series-2);\n  }\n  .row[data-kind=\"retrieval\"] {\n    --_kind: var(--pui-series-3);\n  }\n  .row[data-kind=\"guardrail\"] {\n    --_kind: var(--pui-series-4);\n  }\n  .row[data-kind=\"agent\"],\n  .row[data-kind=\"wait\"] {\n    --_kind: var(--pui-series-8);\n  }\n  /* marks on the lane: a lane-wide box moved to the start and scaled to the length (one transform; a replay grows\n     them without layout) */\n  .bar,\n  .attempt,\n  .ghostBar {\n    inset-inline-start: 0;\n    width: 100%;\n    transform-origin: left;\n    transform: translateX(calc(var(--s) * 100%)) scaleX(var(--w));\n    transition: transform var(--pui-duration-fast) linear;\n  }\n  .bar:dir(rtl),\n  .attempt:dir(rtl),\n  .ghostBar:dir(rtl) {\n    transform-origin: right;\n    transform: translateX(calc(var(--s) * -100%)) scaleX(var(--w));\n  }\n  .bar,\n  .attempt {\n    position: absolute;\n    top: 50%;\n    height: 0.625rem;\n    border-radius: calc(var(--pui-radius-chip) / 3);\n    translate: 0 -50%;\n  }\n  .bar:dir(rtl),\n  .attempt:dir(rtl) {\n    translate: 0 -50%;\n  }\n  .bar {\n    background: var(--_kind, var(--pui-series-8));\n  }\n  .row[data-status=\"queued\"] .name {\n    color: var(--pui-ink-3);\n  }\n  .row[data-kind=\"agent\"] .bar {\n    height: 0.375rem;\n  }\n  .row[data-status=\"running\"] .bar {\n    background: linear-gradient(to right, var(--_kind), color-mix(in oklab, var(--_kind) 55%, var(--pui-surface)));\n  }\n  .row[data-status=\"error\"] .bar {\n    background: var(--pui-danger);\n  }\n  .row[data-status=\"cancelled\"] .bar {\n    background: var(--pui-line-strong);\n  }\n  /* a ghost re-run: the real bar rides the upper half, the ghost an outline in the same colour under it */\n  .row[data-ghost] .bar,\n  .row[data-ghost] .attempt {\n    translate: 0 -95%;\n    height: 0.5rem;\n  }\n  .ghostBar {\n    position: absolute;\n    top: 50%;\n    height: 0.5rem;\n    translate: 0 5%;\n    border-radius: calc(var(--pui-radius-chip) / 3);\n    box-shadow: inset 0 0 0 1px var(--_kind, var(--pui-series-8));\n    background: repeating-linear-gradient(\n      90deg,\n      color-mix(in oklab, var(--_kind, var(--pui-series-8)) 28%, transparent) 0 3px,\n      transparent 3px 5px\n    );\n  }\n  .row[data-ghost=\"only\"] .ghostBar {\n    translate: 0 -50%;\n  }\n  .row[data-shared] .bar {\n    opacity: 0.55;\n  }\n  /* the chain that sets the run's length: a hairline under the bar (and under the ghost's own chain) */\n  .row[data-critical] .bar::after,\n  .ghostBar[data-critical]::after {\n    content: \"\";\n    position: absolute;\n    inset-inline: 0;\n    inset-block-end: -3px;\n    height: 1px;\n    /* meaning never rides the look's accent (red in Pop): the chain is drawn in ink */\n    background: var(--pui-ink);\n  }\n  .delta {\n    font-size: var(--pui-text-2xs);\n    font-variant-numeric: tabular-nums;\n    white-space: nowrap;\n  }\n  .delta[data-tone=\"good\"] {\n    color: var(--pui-success);\n  }\n  .delta[data-tone=\"warn\"] {\n    color: var(--pui-warning);\n  }\n  .delta[data-tone=\"mixed\"] {\n    color: var(--pui-ink-2);\n  }\n  .ghostWord,\n  .sameWord {\n    font-size: var(--pui-text-2xs);\n    color: var(--pui-ink-3);\n    white-space: nowrap;\n  }\n  /* a failed earlier attempt: hatched in the danger tone, with a gap before the next try */\n  .attempt {\n    background: repeating-linear-gradient(135deg, var(--pui-danger) 0 2px, transparent 2px 5px);\n    box-shadow: inset 0 0 0 1px var(--pui-danger);\n  }\n  .row[data-below] .name::after {\n    content: \"\";\n    width: 6px;\n    height: 6px;\n    flex-shrink: 0;\n    border-radius: 50%;\n    background: var(--pui-danger);\n  }\n  .duration {\n    display: flex;\n    align-items: center;\n    justify-content: flex-end;\n    gap: var(--pui-space-1);\n    color: var(--pui-ink-3);\n    font-size: var(--pui-text-2xs);\n    white-space: nowrap;\n  }\n  .failedWord {\n    color: var(--pui-danger);\n  }\n\n  /* the detail: beside the trace when wide, under it when narrow */\n  .detail {\n    min-width: 0;\n    border-top: 1px solid var(--pui-line-soft);\n  }\n  @container (min-width: 54rem) {\n    .detail {\n      border-top: none;\n      border-inline-start: 1px solid var(--pui-line-soft);\n    }\n  }\n  .detailBody {\n    display: grid;\n    gap: var(--pui-space-3);\n    padding: var(--pui-space-4);\n    animation: in var(--pui-motion-in) var(--pui-ease-default);\n  }\n  @keyframes in {\n    from {\n      opacity: 0;\n    }\n  }\n  .detailTitle {\n    display: flex;\n    align-items: center;\n    gap: var(--pui-space-2);\n    color: var(--pui-ink);\n    font-size: var(--pui-text-sm);\n    font-weight: var(--pui-weight-medium);\n  }\n  .detailFacts {\n    display: grid;\n    grid-template-columns: auto minmax(0, 1fr);\n    gap: var(--pui-space-1) var(--pui-space-3);\n    font-size: var(--pui-text-xs);\n  }\n  .detailFacts dt {\n    color: var(--pui-ink-3);\n  }\n  .detailFacts dd {\n    color: var(--pui-ink);\n  }\n  .blockLabel {\n    margin-bottom: var(--pui-space-1);\n    color: var(--pui-ink-3);\n    font-size: var(--pui-text-2xs);\n    font-weight: var(--pui-weight-medium);\n    letter-spacing: var(--pui-tracking-mono-caps, 0.04em);\n    text-transform: uppercase;\n  }\n  .attempts ol {\n    display: grid;\n    gap: var(--pui-space-1);\n    padding-inline-start: var(--pui-space-4);\n    color: var(--pui-ink-2);\n    font-size: var(--pui-text-xs);\n    list-style: decimal;\n  }\n  .block {\n    max-height: 12rem;\n    overflow: auto;\n    padding: var(--pui-space-2) var(--pui-space-3);\n    border-radius: var(--pui-radius-control);\n    color: var(--pui-ink-2);\n    font-family: var(--pui-font-code);\n    font-size: var(--pui-text-2xs);\n    line-height: 1.5;\n    text-align: left;\n    white-space: pre-wrap;\n  }\n  .block[data-error] {\n    color: var(--pui-danger);\n  }\n}\n"},{"path":"beamline/components/run-trace/run-trace.tsx","type":"registry:file","target":"src/ui/beamline/components/run-trace/run-trace.tsx","content":"\"use client\";\nimport { createFormatters, FALLBACK_LOCALE } from \"../../runtime/messages\";\nimport { type Translator } from \"../../runtime/messages\";\nimport { numberFormatter, useMessages } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./messages\";\nimport { useLocale } from \"../../runtime/localization\";\n\nimport { useMemo, useRef, useState, type CSSProperties, type KeyboardEvent, type ReactNode, type Ref } from \"react\";\nimport {\n  Ban,\n  Bot,\n  ChevronRight,\n  CircleCheck,\n  CircleX,\n  Database,\n  Hourglass,\n  ShieldCheck,\n  Sparkles,\n  Wrench,\n} from \"lucide-react\";\nimport { Spinner } from \"../spinner/spinner\";\nimport { useActive } from \"../../hooks/use-active\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { useFlip } from \"../../hooks/use-flip\";\nimport { useWidthAs } from \"../../hooks/use-content-width\";\nimport { useNow } from \"../../lib/now\";\nimport { cn } from \"../../lib/utils\";\nimport { useControllable } from \"../../hooks/use-controllable\";\nimport styles from \"./run-trace.module.css\";\n\nexport type TraceSpanKind = \"agent\" | \"llm\" | \"tool\" | \"retrieval\" | \"guardrail\" | \"wait\";\n/** `queued`: not started yet (a replay before the step begins): its row holds its place, with no bar and no time. */\nexport type TraceSpanStatus = \"ok\" | \"error\" | \"running\" | \"cancelled\" | \"queued\";\n\nexport interface TraceSpan {\n  id: string;\n  /** The step that started this one; omit for the run's top steps. */\n  parentId?: string | null;\n  name: string;\n  kind?: TraceSpanKind;\n  /** ms from the start of the run. */\n  start: number;\n  /** ms from the start of the run; missing while it runs. */\n  end?: number | null;\n  status?: TraceSpanStatus;\n  /** Earlier tries that failed before the one that counts (start/end in ms from the run's start). */\n  attempts?: { start: number; end: number; error?: string }[];\n  /** null when the provider did not report them. */\n  tokens?: { input: number; output: number } | null;\n  /** In the money of `currency`; null when not reported. */\n  cost?: number | null;\n  model?: string;\n  input?: unknown;\n  output?: unknown;\n  error?: string;\n}\n\n/** A second run of the same steps from one changed step (a ghost re-run), drawn under the real run on the same axis. */\nexport interface TraceGhost {\n  /** The ghost's steps, matched to the real run's by id. A real step after `from` with no ghost counterpart is one\n   *  the ghost didn't run (a retry it skipped shows as attempts it doesn't have); a ghost step with a new id is one\n   *  only the ghost ran. */\n  spans: TraceSpan[];\n  /** The step that was changed: real steps that ended before it began were reused, not run again (\"same\"). */\n  from: string;\n  /** How a ghost step got its result when it wasn't run live, by id: \"recorded\", \"dry run\". */\n  notes?: Record<string, string>;\n}\n\nexport type RunTraceClassNames = Partial<\n  Record<\n    \"root\" | \"summary\" | \"axis\" | \"row\" | \"name\" | \"bar\" | \"attempt\" | \"duration\" | \"detail\" | \"ghost\" | \"delta\",\n    string\n  >\n>;\n\nexport interface RunTraceProps {\n  spans: TraceSpan[];\n  /** ms since the run started; omit to let the trace keep its own clock while something runs. */\n  now?: number;\n  /** The time axis's length in ms; omit to fit the run so far. Give the whole run's length while a replay rewinds it\n   *  (the Replay bar's `frozen`), so the bars stay in place and only the line at now moves. */\n  extent?: number;\n  title?: ReactNode;\n  selected?: string | null;\n  defaultSelected?: string | null;\n  onSelectedChange?: (id: string | null) => void;\n  expanded?: string[];\n  defaultExpanded?: string[];\n  onExpandedChange?: (ids: string[]) => void;\n  density?: \"compact\" | \"default\";\n  currency?: string;\n  /** A ghost re-run to draw under the real run: dashed bars on the same axis, each step's time and cost difference. */\n  ghost?: TraceGhost;\n  /** Mark the critical path (the chain of steps that set the run's length), and the ghost's. */\n  criticalPath?: boolean;\n  /** More in the chosen step's detail, after its output: a re-run picker, links. */\n  detailExtra?: (span: TraceSpan) => ReactNode;\n  className?: string;\n  classNames?: RunTraceClassNames;\n  style?: CSSProperties;\n  ref?: Ref<HTMLDivElement>;\n}\nfunction localizedKIND(\n  i18n: Translator<typeof englishMessages>,\n): Record<TraceSpanKind, { label: string; icon: ReactNode }> {\n  return {\n    agent: { label: i18n(\"agent\"), icon: <Bot /> },\n    llm: { label: i18n(\"model\"), icon: <Sparkles /> },\n    tool: { label: i18n(\"tool\"), icon: <Wrench /> },\n    retrieval: { label: i18n(\"retrieval\"), icon: <Database /> },\n    guardrail: { label: i18n(\"check\"), icon: <ShieldCheck /> },\n    wait: { label: i18n(\"wait\"), icon: <Hourglass /> },\n  };\n}\nconst duration = (ms: number, locale = FALLBACK_LOCALE) => {\n  const n = createFormatters(locale).number;\n  if (ms < 1000) return n(Math.round(ms), { style: \"unit\", unit: \"millisecond\", unitDisplay: \"narrow\" });\n  if (ms < 60000)\n    return n(ms / 1000, {\n      style: \"unit\",\n      unit: \"second\",\n      unitDisplay: \"narrow\",\n      minimumFractionDigits: ms < 10000 ? 2 : 1,\n      maximumFractionDigits: ms < 10000 ? 2 : 1,\n    });\n  return (\n    n(Math.floor(ms / 60000), { style: \"unit\", unit: \"minute\", unitDisplay: \"narrow\" }) +\n    \" \" +\n    n(Math.round((ms % 60000) / 1000), { style: \"unit\", unit: \"second\", unitDisplay: \"narrow\" })\n  );\n};\nfunction createLocalFormatters(locale: string) {\n  const compact = numberFormatter(locale, { notation: \"compact\", maximumFractionDigits: 1 });\n  return { compact };\n}\nfunction useLocalFormatters(override?: string) {\n  const locale = useLocale(override);\n  return useMemo(() => createLocalFormatters(locale), [locale]);\n}\n\n/** A \"nice\" step for about `n` marks over `span` ms. */\nfunction tickStep(span: number, n: number) {\n  const raw = span / Math.max(1, n);\n  const pow = 10 ** Math.floor(Math.log10(raw));\n  const m = raw / pow;\n  return (m <= 1 ? 1 : m <= 2 ? 2 : m <= 5 ? 5 : 10) * pow;\n}\nconst show = (v: unknown) => (v == null ? null : typeof v === \"string\" ? v : JSON.stringify(v, null, 2));\n\n/**\n * One agent run as a waterfall: steps nested under the step that started them, each a bar on a shared time axis,\n * retries as earlier attempts on the same bar, tokens and cost per step and for the run. Live bars grow while a step\n * runs (a clock that stops when nothing runs or the trace is off screen); choosing a step shows its detail.\n */\nexport function RunTrace({\n  spans,\n  now: nowProp,\n  extent,\n  title,\n  selected: selectedProp,\n  defaultSelected = null,\n  onSelectedChange,\n  expanded: expandedProp,\n  defaultExpanded,\n  onExpandedChange,\n  density = \"compact\",\n  currency = \"$\",\n  ghost,\n  criticalPath = false,\n  detailExtra,\n  className,\n  classNames,\n  style,\n  ref,\n}: RunTraceProps) {\n  const i18n = useMessages(\"run-trace\", englishMessages);\n  const kinds = useMemo(() => localizedKIND(i18n), [i18n]);\n\n  const { compact } = useLocalFormatters();\n\n  const root = useRef<HTMLDivElement>(null);\n  const attach = useComposedRef(root, ref);\n  const active = useActive(root);\n  const running = spans.some((s) => s.end == null && s.status !== \"cancelled\" && s.status !== \"error\");\n  // the trace's own clock: from the latest time it has been told, forward in real time, only while something runs\n  const anchor = useRef<{ at: number; base: number } | null>(null);\n  const known = Math.max(0, ...spans.map((s) => s.end ?? s.start));\n  const wall = useNow(nowProp === undefined && running && active, 100);\n  if (wall != null && (!anchor.current || known > anchor.current.base)) anchor.current = { at: wall, base: known };\n  const now =\n    nowProp ?? (running && wall != null && anchor.current ? anchor.current.base + (wall - anchor.current.at) : known);\n\n  const [selected, select] = useControllable<string | null>(selectedProp, defaultSelected, onSelectedChange);\n  const realIds = useMemo(() => new Set(spans.map((s) => s.id)), [spans]);\n  const ghostById = useMemo(() => new Map((ghost?.spans ?? []).map((s) => [s.id, s])), [ghost]);\n  // steps only the ghost ran join the tree under their parent, drawn as ghosts alone\n  const ghostOnly = useMemo(() => (ghost?.spans ?? []).filter((s) => !realIds.has(s.id)), [ghost, realIds]);\n  const childrenOf = useMemo(() => {\n    const m = new Map<string | null, TraceSpan[]>();\n    for (const s of [...spans, ...ghostOnly]) {\n      const k = s.parentId ?? null;\n      m.set(k, [...(m.get(k) ?? []), s]);\n    }\n    for (const list of m.values()) list.sort((a, b) => a.start - b.start);\n    return m;\n  }, [spans, ghostOnly]);\n  const [openIds, setOpenIds] = useControllable<string[] | null, string[]>(\n    expandedProp,\n    defaultExpanded ?? null,\n    onExpandedChange,\n  );\n  const isOpen = (id: string) => openIds === null || openIds.includes(id);\n  const setOpen = (id: string, open: boolean) => {\n    const all = openIds ?? spans.filter((s) => childrenOf.has(s.id)).map((s) => s.id);\n    const next = open ? [...new Set([...all, id])] : all.filter((x) => x !== id);\n    setOpenIds(next);\n  };\n\n  // the visible rows, depth first, under open parents only\n  const rows = useMemo(() => {\n    const out: { span: TraceSpan; depth: number }[] = [];\n    const walk = (parent: string | null, depth: number) => {\n      for (const s of childrenOf.get(parent) ?? []) {\n        out.push({ span: s, depth });\n        if (isOpen(s.id)) walk(s.id, depth + 1);\n      }\n    };\n    walk(null, 0);\n    return out;\n  }, [childrenOf, openIds]); // eslint-disable-line react-hooks/exhaustive-deps -- isOpen reads openIds\n  const byId = useMemo(() => new Map(spans.map((s) => [s.id, s])), [spans]);\n  const endOf = (s: TraceSpan) => s.end ?? (s.status === \"cancelled\" || s.status === \"error\" ? s.start : now);\n  const statusOf = (s: TraceSpan): TraceSpanStatus => s.status ?? (s.end == null ? \"running\" : \"ok\");\n  // how long the run has taken, and the axis it is drawn on (the same unless `extent` holds it longer)\n  const elapsed = Math.max(1, now, ...spans.filter((s) => s.status !== \"queued\").map(endOf));\n  const ghostEnd = (s: TraceSpan) => s.end ?? (s.status === \"cancelled\" || s.status === \"error\" ? s.start : now);\n  const total = Math.max(elapsed, extent ?? 0, ...(ghost?.spans ?? []).map(ghostEnd));\n  const fromStart = ghost ? (byId.get(ghost.from)?.start ?? ghostById.get(ghost.from)?.start ?? 0) : 0;\n  // the chain of steps that set a run's length: the top step that ends last, then its child that ends last, and down\n  const critical = (list: TraceSpan[], end: (s: TraceSpan) => number) => {\n    const kids = new Map<string | null, TraceSpan[]>();\n    for (const s of list) kids.set(s.parentId ?? null, [...(kids.get(s.parentId ?? null) ?? []), s]);\n    const out = new Set<string>();\n    let level = kids.get(null) ?? [];\n    while (level.length) {\n      const last = level.reduce((a, b) => (end(b) > end(a) ? b : a));\n      out.add(last.id);\n      level = kids.get(last.id) ?? [];\n    }\n    return out;\n  };\n  const realCritical = useMemo(\n    () => (criticalPath ? critical(spans, endOf) : new Set<string>()),\n    [criticalPath, spans, now], // eslint-disable-line react-hooks/exhaustive-deps -- endOf reads now\n  );\n  const ghostCritical = useMemo(\n    () => (criticalPath && ghost ? critical(ghost.spans, ghostEnd) : new Set<string>()),\n    [criticalPath, ghost, now], // eslint-disable-line react-hooks/exhaustive-deps -- ghostEnd reads now\n  );\n  // a failing step marks the steps above it, so a closed parent still says something under it failed\n  const failedBelow = useMemo(() => {\n    const set = new Set<string>();\n    for (const s of spans)\n      if (statusOf(s) === \"error\") {\n        let p = s.parentId;\n        while (p) {\n          set.add(p);\n          p = byId.get(p)?.parentId;\n        }\n      }\n    return set;\n  }, [spans, byId]);\n\n  const room = useWidthAs(root, (w) => (w < 560 ? \"narrow\" : w < 860 ? \"mid\" : \"wide\"), \"wide\");\n  // as many time labels as the lane holds (one per 64 px, a label's width and a gap), at most three narrow and five wide\n  const laneRef = useRef<HTMLSpanElement>(null);\n  const lane = useWidthAs(laneRef, (w) => Math.floor(w / 64), -1);\n  const step = tickStep(total, Math.max(1, Math.min(room === \"narrow\" ? 3 : 5, lane < 0 ? Infinity : lane)));\n  const ticks = Array.from({ length: Math.floor(total / step) + 1 }, (_, i) => i * step);\n  const pct = (ms: number) => `${(ms / total) * 100}%`;\n  // a mark on the lane: its start and length as fractions, drawn as one transform (a lane-wide box moved and scaled),\n  // so a replay grows bars and moves the now line without laying anything out\n  const mark = (start: number, length: number) =>\n    ({ \"--s\": start / total, \"--w\": Math.max(length, total / 400) / total }) as CSSProperties;\n\n  const errors = spans.filter((s) => statusOf(s) === \"error\").length;\n  const tokens = spans.reduce(\n    (t, s) => (s.tokens ? { input: t.input + s.tokens.input, output: t.output + s.tokens.output, any: true } : t),\n    { input: 0, output: 0, any: false },\n  );\n  const reportedCost = spans.filter((s) => s.cost != null);\n  const cost = reportedCost.reduce((c, s) => c + (s.cost ?? 0), 0);\n  const runStatus: TraceSpanStatus = running ? \"running\" : errors ? \"error\" : \"ok\";\n\n  const list = useRef<HTMLDivElement>(null);\n  useFlip(list, { ghost: false });\n  const [cursor, setCursor] = useState<string | null>(null);\n  const at = rows.find((r) => r.span.id === (cursor ?? selected))?.span.id ?? rows[0]?.span.id ?? null;\n  const focusRow = (id: string) => {\n    setCursor(id);\n    requestAnimationFrame(() => list.current?.querySelector<HTMLElement>(`[data-id=\"${CSS.escape(id)}\"]`)?.focus());\n  };\n  const onKey = (e: KeyboardEvent<HTMLDivElement>) => {\n    if (!at) return;\n    const i = rows.findIndex((r) => r.span.id === at);\n    const span = rows[i]?.span;\n    const rtl = getComputedStyle(e.currentTarget).direction === \"rtl\";\n    const forward = rtl ? \"ArrowLeft\" : \"ArrowRight\";\n    const back = rtl ? \"ArrowRight\" : \"ArrowLeft\";\n    if (e.key === \"ArrowDown\" && rows[i + 1]) focusRow(rows[i + 1].span.id);\n    else if (e.key === \"ArrowUp\" && rows[i - 1]) focusRow(rows[i - 1].span.id);\n    else if (e.key === \"Home\" && rows[0]) focusRow(rows[0].span.id);\n    else if (e.key === \"End\" && rows.length) focusRow(rows[rows.length - 1].span.id);\n    else if (e.key === forward && span) {\n      if (childrenOf.has(span.id) && !isOpen(span.id)) setOpen(span.id, true);\n      else if (childrenOf.has(span.id) && rows[i + 1]) focusRow(rows[i + 1].span.id);\n    } else if (e.key === back && span) {\n      if (childrenOf.has(span.id) && isOpen(span.id)) setOpen(span.id, false);\n      else if (span.parentId) focusRow(span.parentId);\n    } else if ((e.key === \"Enter\" || e.key === \" \") && span) select(selected === span.id ? null : span.id);\n    else if (e.key === \"Escape\" && selected) select(null);\n    else return;\n    e.preventDefault();\n  };\n\n  const chosen = selected ? byId.get(selected) : undefined;\n  return (\n    <div\n      ref={attach}\n      data-slot=\"run-trace\"\n      data-room={room}\n      data-density={density}\n      data-state={runStatus}\n      data-ghosted={ghost ? \"\" : undefined}\n      style={style}\n      className={cn(\"m-surface\", styles.root, classNames?.root, className)}\n    >\n      <div data-slot=\"run-trace-summary\" className={cn(styles.summary, classNames?.summary)}>\n        <span className={styles.runStatus} data-status={runStatus}>\n          {runStatus === \"running\" ? (\n            <Spinner size=\"sm\" label={i18n(\"running\")} />\n          ) : runStatus === \"error\" ? (\n            <CircleX aria-hidden />\n          ) : (\n            <CircleCheck aria-hidden />\n          )}\n          {runStatus === \"running\" ? i18n(\"running\") : runStatus === \"error\" ? i18n(\"failed\") : i18n(\"succeeded\")}\n        </span>\n        {title && <span className={styles.title}>{title}</span>}\n        <dl className={styles.facts}>\n          <div>\n            <dt>{i18n(\"time\")}</dt>\n            <dd className=\"tabular-nums\">{duration(elapsed, i18n.locale)}</dd>\n          </div>\n          <div>\n            <dt>{i18n(\"steps\")}</dt>\n            <dd>\n              {spans.length}\n              {errors > 0 && <span className={styles.errCount}> {i18n(\"failed2\", { errors: errors })}</span>}\n            </dd>\n          </div>\n          <div>\n            <dt>{i18n(\"tokens\")}</dt>\n            <dd className=\"tabular-nums\">\n              {tokens.any\n                ? i18n(\"inOut\", {\n                    value: compact.format(tokens.input),\n                    value2: compact.format(tokens.output),\n                  })\n                : i18n(\"notReported\")}\n            </dd>\n          </div>\n          <div>\n            <dt>{i18n(\"cost\")}</dt>\n            <dd className=\"tabular-nums\">\n              {reportedCost.length\n                ? `${currency}${i18n.number(cost, { minimumFractionDigits: cost < 1 ? 4 : 2, maximumFractionDigits: cost < 1 ? 4 : 2 })}`\n                : i18n(\"notReported\")}\n            </dd>\n          </div>\n        </dl>\n      </div>\n\n      <div className={styles.body}>\n        <div className={styles.trace}>\n          <div aria-hidden data-slot=\"run-trace-axis\" className={cn(styles.axis, classNames?.axis)}>\n            <span />\n            <span ref={laneRef} className={styles.lane}>\n              {ticks.map((t) => (\n                <span key={t} className={styles.tick} style={{ insetInlineStart: pct(t) }}>\n                  {duration(t, i18n.locale)}\n                </span>\n              ))}\n            </span>\n          </div>\n          {rows.length === 0 ? (\n            <p className={styles.empty}>{i18n(\"noStepsYetTheyAppearHereAs\")}</p>\n          ) : (\n            <div\n              ref={list}\n              role=\"treegrid\"\n              aria-label={typeof title === \"string\" ? i18n(\"trace\", { title: title }) : i18n(\"runTrace\")}\n              aria-readonly\n              className={styles.rows}\n              onKeyDown={onKey}\n            >\n              {running && (\n                <div aria-hidden className={styles.nowOverlay}>\n                  <span />\n                  <span className={styles.lane}>\n                    <span className={styles.nowLine} style={{ \"--s\": now / total } as CSSProperties} />\n                  </span>\n                </div>\n              )}\n              {rows.map(({ span: s, depth }) => {\n                const g = ghost ? ghostById.get(s.id) : undefined;\n                const onlyGhost = !!ghost && !realIds.has(s.id);\n                // shared: ended before the changed step began, so the ghost reused it\n                const shared = !!ghost && !onlyGhost && endOf(s) <= fromStart && s.id !== ghost.from;\n                const skipped = !!ghost && !onlyGhost && !shared && !g && s.start >= fromStart;\n                const gms = g ? Math.max(0, ghostEnd(g) - g.start) : 0;\n                const st = statusOf(s);\n                const kind = s.kind ?? \"tool\";\n                const hasKids = childrenOf.has(s.id);\n                const end = endOf(s);\n                const ms = Math.max(0, end - s.start);\n                const open = hasKids && isOpen(s.id);\n                return (\n                  <div\n                    key={s.id}\n                    data-flip={s.id}\n                    data-id={s.id}\n                    role=\"row\"\n                    aria-level={depth + 1}\n                    aria-expanded={hasKids ? open : undefined}\n                    aria-selected={selected === s.id}\n                    tabIndex={s.id === at ? 0 : -1}\n                    data-slot=\"run-trace-row\"\n                    data-status={st}\n                    data-kind={kind}\n                    data-below={failedBelow.has(s.id) && !open ? \"\" : undefined}\n                    data-ghost={g || onlyGhost ? (onlyGhost ? \"only\" : \"\") : undefined}\n                    data-shared={shared ? \"\" : undefined}\n                    data-critical={realCritical.has(s.id) ? \"\" : undefined}\n                    className={cn(styles.row, classNames?.row)}\n                    style={{ \"--_depth\": depth } as CSSProperties}\n                    onClick={() => {\n                      setCursor(s.id);\n                      select(selected === s.id ? null : s.id);\n                    }}\n                    onFocus={() => setCursor(s.id)}\n                  >\n                    <span role=\"gridcell\" data-slot=\"run-trace-name\" className={cn(styles.name, classNames?.name)}>\n                      <span className={styles.indent} />\n                      {hasKids ? (\n                        <button\n                          type=\"button\"\n                          tabIndex={-1}\n                          aria-label={open ? i18n(\"close\", { name: s.name }) : i18n(\"open\", { name: s.name })}\n                          className={styles.chevron}\n                          data-open={open || undefined}\n                          onClick={(e) => {\n                            e.stopPropagation();\n                            setOpen(s.id, !open);\n                          }}\n                        >\n                          <ChevronRight aria-hidden />\n                        </button>\n                      ) : (\n                        <span className={styles.chevronSpace} />\n                      )}\n                      <span aria-hidden className={styles.kindIcon}>\n                        {kinds[kind].icon}\n                      </span>\n                      <span className={styles.label}>{s.name}</span>\n                      {s.model && <span className={styles.model}>{s.model}</span>}\n                      <span className=\"sr-only\">\n                        , {kinds[kind].label},{\" \"}\n                        {st === \"error\"\n                          ? i18n(\"failed3\")\n                          : st === \"running\"\n                            ? i18n(\"running2\")\n                            : st === \"cancelled\"\n                              ? i18n(\"cancelled\")\n                              : i18n(\"done\")}\n                        , {duration(ms, i18n.locale)}\n                        {s.attempts?.length ? i18n(\"attempts\", { value: s.attempts.length + 1 }) : \"\"}\n                        {g ? `, ${i18n(\"ghostSays\", { value: duration(gms, i18n.locale) })}` : \"\"}\n                        {shared ? `, ${i18n(\"same\")}` : skipped ? `, ${i18n(\"notRun\")}` : \"\"}\n                      </span>\n                    </span>\n                    <span role=\"gridcell\" aria-hidden className={styles.lane}>\n                      {g && (\n                        <span\n                          data-slot=\"run-trace-ghost\"\n                          data-critical={ghostCritical.has(s.id) ? \"\" : undefined}\n                          className={cn(styles.ghostBar, classNames?.ghost)}\n                          style={mark(g.start, gms)}\n                        />\n                      )}\n                      {!onlyGhost &&\n                        s.attempts?.map((a, i) => (\n                          <span\n                            key={i}\n                            data-slot=\"run-trace-attempt\"\n                            className={cn(styles.attempt, classNames?.attempt)}\n                            style={mark(a.start, a.end - a.start)}\n                            title={a.error}\n                          />\n                        ))}\n                      {!onlyGhost && st !== \"queued\" && (\n                        <span\n                          data-slot=\"run-trace-bar\"\n                          className={cn(styles.bar, classNames?.bar)}\n                          style={mark(s.start, ms)}\n                        />\n                      )}\n                    </span>\n                    <span\n                      role=\"gridcell\"\n                      data-slot=\"run-trace-duration\"\n                      className={cn(styles.duration, classNames?.duration)}\n                    >\n                      {st === \"error\" ? (\n                        <span className={styles.failedWord}>{i18n(\"failed\")}</span>\n                      ) : st === \"cancelled\" ? (\n                        <Ban aria-hidden className=\"size-3\" />\n                      ) : null}\n                      {st === \"queued\" && !onlyGhost ? (\n                        <span className={styles.ghostWord}>{i18n(\"queued\")}</span>\n                      ) : (\n                        <span className=\"tabular-nums\">{duration(onlyGhost ? gms : ms, i18n.locale)}</span>\n                      )}\n                      {g && !onlyGhost && g.end != null && s.end != null && (\n                        <Delta\n                          ms={gms - ms}\n                          cost={g.cost != null && s.cost != null ? g.cost - s.cost : null}\n                          note={ghost?.notes?.[s.id]}\n                          currency={currency}\n                          className={classNames?.delta}\n                        />\n                      )}\n                      {onlyGhost && <span className={styles.ghostWord}>{i18n(\"ghostOnly\")}</span>}\n                      {shared && <span className={styles.sameWord}>{i18n(\"same\")}</span>}\n                      {skipped && <span className={styles.ghostWord}>{i18n(\"notRun\")}</span>}\n                    </span>\n                  </div>\n                );\n              })}\n            </div>\n          )}\n        </div>\n        {chosen && (\n          <Detail\n            span={chosen}\n            end={endOf(chosen)}\n            status={statusOf(chosen)}\n            currency={currency}\n            className={classNames?.detail}\n            extra={detailExtra?.(chosen)}\n          />\n        )}\n      </div>\n    </div>\n  );\n}\n\n/** A ghost step against its real step: time and cost, signed by what it does to the run, in words as well as tone. */\nfunction Delta({\n  ms,\n  cost,\n  note,\n  currency,\n  className,\n}: {\n  ms: number;\n  cost: number | null;\n  note?: string;\n  currency: string;\n  className?: string;\n}) {\n  const i18n = useMessages(\"run-trace\", englishMessages);\n  const sign = (n: number) => (n > 0 ? \"+\" : n < 0 ? \"−\" : \"±\");\n  const time = Math.abs(ms) < 50 ? null : `${sign(ms)}${duration(Math.abs(ms), i18n.locale)}`;\n  const money =\n    cost == null || Math.abs(cost) < 0.00005\n      ? null\n      : `${sign(cost)}${currency}${i18n.number(Math.abs(cost), { minimumFractionDigits: 2, maximumFractionDigits: Math.abs(cost) < 0.01 ? 4 : 2 })}`;\n  const better = ms <= 0 && (cost == null || cost <= 0);\n  const worse = ms >= 0 && (cost == null || cost >= 0) && (time || money);\n  return (\n    <span\n      data-slot=\"run-trace-delta\"\n      data-tone={better ? \"good\" : worse ? \"warn\" : \"mixed\"}\n      className={cn(styles.delta, className)}\n    >\n      {[time, money].filter(Boolean).join(\" · \") || i18n(\"noDifference\")}\n      {note && <span className={styles.ghostWord}> · {note}</span>}\n    </span>\n  );\n}\n\nfunction Detail({\n  span,\n  end,\n  status,\n  currency,\n  className,\n  extra,\n}: {\n  span: TraceSpan;\n  end: number;\n  status: TraceSpanStatus;\n  currency: string;\n  className?: string;\n  extra?: ReactNode;\n}) {\n  const i18n = useMessages(\"run-trace\", englishMessages);\n  const kinds = useMemo(() => localizedKIND(i18n), [i18n]);\n\n  const locale = useLocale();\n\n  const kind = span.kind ?? \"tool\";\n  const input = show(span.input);\n  const output = show(span.error ?? span.output);\n  return (\n    <aside\n      data-slot=\"run-trace-detail\"\n      aria-label={i18n(\"details\", { name: span.name })}\n      className={cn(styles.detail, className)}\n    >\n      <div key={span.id} className={styles.detailBody}>\n        <p className={styles.detailTitle}>\n          <span aria-hidden className={styles.kindIcon}>\n            {kinds[kind].icon}\n          </span>\n          {span.name}\n        </p>\n        <dl className={styles.detailFacts}>\n          <dt>{i18n(\"kind\")}</dt>\n          <dd>\n            {kinds[kind].label}\n            {span.model ? ` · ${span.model}` : \"\"}\n          </dd>\n          <dt>{i18n(\"started\")}</dt>\n          <dd className=\"tabular-nums\">{i18n(\"at\", { value: duration(span.start, i18n.locale) })}</dd>\n          <dt>{i18n(\"took\")}</dt>\n          <dd className=\"tabular-nums\">\n            {duration(Math.max(0, end - span.start), i18n.locale)}\n            {status === \"running\" ? i18n(\"soFar\") : \"\"}\n          </dd>\n          <dt>{i18n(\"tokens\")}</dt>\n          <dd className=\"tabular-nums\">\n            {span.tokens\n              ? i18n(\"inOut\", {\n                  value: numberFormatter(locale).format(span.tokens.input),\n                  value2: numberFormatter(locale).format(span.tokens.output),\n                })\n              : span.kind === \"llm\"\n                ? i18n(\"notReported\")\n                : \"—\"}\n          </dd>\n          <dt>{i18n(\"cost\")}</dt>\n          <dd className=\"tabular-nums\">\n            {span.cost != null\n              ? `${currency}${i18n.number(span.cost, { minimumFractionDigits: 4, maximumFractionDigits: 4 })}`\n              : span.kind === \"llm\"\n                ? i18n(\"notReported\")\n                : \"—\"}\n          </dd>\n        </dl>\n        {!!span.attempts?.length && (\n          <div className={styles.attempts}>\n            <p className={styles.blockLabel}>{i18n(\"attempts2\")}</p>\n            <ol>\n              {span.attempts.map((a, i) => (\n                <li key={i}>\n                  <span className={styles.failedWord}>{i18n(\"failed\")}</span>{\" \"}\n                  {i18n(\"after\", { value: duration(a.end - a.start, i18n.locale) })}\n                  {a.error ? `: ${a.error}` : \"\"}\n                </li>\n              ))}\n              <li>\n                {status === \"error\" ? (\n                  <span className={styles.failedWord}>{i18n(\"failed\")}</span>\n                ) : status === \"running\" ? (\n                  i18n(\"running\")\n                ) : (\n                  i18n(\"succeeded\")\n                )}{\" \"}\n                {i18n(\"after\", { value: duration(Math.max(0, end - span.start), i18n.locale) })}\n              </li>\n            </ol>\n          </div>\n        )}\n        {input && (\n          <div>\n            <p className={styles.blockLabel}>{i18n(\"input\")}</p>\n            <pre dir=\"ltr\" className={cn(\"m-inset\", styles.block)}>\n              {input}\n            </pre>\n          </div>\n        )}\n        {output && (\n          <div>\n            <p className={styles.blockLabel}>{span.error ? i18n(\"error\") : i18n(\"output\")}</p>\n            <pre dir=\"ltr\" className={cn(\"m-inset\", styles.block)} data-error={span.error ? \"\" : undefined}>\n              {output}\n            </pre>\n          </div>\n        )}\n        {extra}\n      </div>\n    </aside>\n  );\n}\n\nexport default RunTrace;\n"},{"path":"beamline/components/spinner/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/spinner/defaults.ts","content":"/** Pure allowed presentation defaults for Spinner; no component or runtime imports. */\nexport interface SpinnerDefaults {\n  /** Shared size setting; omit to inherit the surrounding scope or the component default. */\n  size?: \"sm\" | \"md\" | \"lg\";\n}\n"},{"path":"beamline/components/spinner/messages.ts","type":"registry:file","target":"src/ui/beamline/components/spinner/messages.ts","content":"import type { MessageDefinitions } from \"../../runtime/messages\";\n/** Built-in English copy; stable keys are the public translation API. */\nexport const english = {\n  label: \"Loading\",\n  done: \"Done\",\n  error: \"Failed\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/spinner/spinner.module.css","type":"registry:file","target":"src/ui/beamline/components/spinner/spinner.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* With a delay the ring waits unseen and then fades in, so quick work never flashes it; the result (a check or a\n     cross) always shows at once. */\n  .root[data-delay] {\n    animation: appear var(--pui-fade-in) var(--pui-ease-default) var(--_delay) both;\n  }\n  @keyframes appear {\n    from {\n      opacity: 0;\n    }\n  }\n  /* The turn runs on the compositor (a CSS animation on the svg box) and costs no JS per frame. When the work ends the\n     arc closes into a full ring over the standard duration, so stopping the turn is invisible, and the check or cross\n     draws itself a beat later. All timing comes from the motion tokens; the loop period follows --pui-motion-scale. */\n  .svg[data-state=\"loading\"] {\n    animation: turn calc(0.8s * var(--pui-motion-scale, 1) * var(--pui-motion-look, 1)) linear infinite;\n  }\n  .track {\n    stroke-opacity: 0.18;\n  }\n  .arc {\n    stroke-dasharray: 0.3 1;\n    transition: stroke-dasharray var(--pui-duration-standard) var(--pui-ease-enter);\n  }\n  .svg:not([data-state=\"loading\"]) .arc {\n    stroke-dasharray: 1 1;\n  }\n  .mark {\n    stroke-dasharray: 1 1;\n    animation: draw var(--pui-duration-standard) var(--pui-ease-enter) var(--pui-duration-fast) both;\n  }\n  @keyframes turn {\n    to {\n      transform: rotate(1turn);\n    }\n  }\n  @keyframes draw {\n    from {\n      stroke-dashoffset: 1;\n    }\n    to {\n      stroke-dashoffset: 0;\n    }\n  }\n  .svg[data-paused] {\n    animation-play-state: paused;\n  }\n  .svg[data-reduced],\n  .svg[data-reduced] .mark {\n    animation: none;\n  }\n  /* Reduction retains the loading/result shape without a decorative loop. */\n  @media (prefers-reduced-motion: reduce) {\n    .svg[data-state=\"loading\"],\n    .mark {\n      animation: none;\n    }\n  }\n}\n"},{"path":"beamline/components/spinner/spinner.tsx","type":"registry:file","target":"src/ui/beamline/components/spinner/spinner.tsx","content":"\"use client\";\n\nimport { useMessages } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./messages\";\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { SpinnerDefaults } from \"./defaults\";\nexport type { SpinnerDefaults } from \"./defaults\";\n\nimport { useRef, type CSSProperties, type Ref } from \"react\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { useActive } from \"../../hooks/use-active\";\nimport { useMotionTokens } from \"../../hooks/use-motion-tokens\";\nimport { useReducedMotionSafe } from \"../../hooks/use-reduced-motion-safe\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./spinner.module.css\";\n\nexport interface SpinnerClassNames {\n  /** Classes for the root part; no override by default. */\n  root?: string;\n  /** Classes for the svg part; no override by default. */\n  svg?: string;\n  /** Classes for the track part; no override by default. */\n  track?: string;\n  /** Classes for the arc part; no override by default. */\n  arc?: string;\n  /** Classes for the mark part; no override by default. */\n  mark?: string;\n}\n\nexport interface SpinnerProps {\n  /** `success` draws a check out of the ring, `error` a cross; both stop spinning. */\n  state?: \"loading\" | \"success\" | \"error\";\n  /** Control size within the supported size steps. Defaults to \"md\". */\n  size?: \"sm\" | \"md\" | \"lg\";\n  /** Accessible name while loading. */\n  label?: string;\n  /** ms to wait before it shows: work that ends sooner never flashes a spinner (about 300 is a good start). Default 0. */\n  delay?: number;\n  /** Classes on the documented visual root; no override by default. */\n  className?: string;\n  /** Receives the documented root element; no ref by default. */\n  ref?: Ref<HTMLSpanElement>;\n  /** Inline styles on the documented visual root; no inline overrides by default. */\n  style?: CSSProperties;\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: SpinnerClassNames;\n}\n\n/** Icon box from the foundation icon steps (§14.2): sm 14, md 16, lg 20. */\nconst PX = { sm: \"var(--pui-icon-sm)\", md: \"var(--pui-icon-md)\", lg: \"var(--pui-icon-lg)\" } as const;\n\n/**\n * A ring that spins while work runs and closes into a drawn check (or cross) when it ends — the result is a shape as\n * well as a colour. With reduced motion the loading ring holds still.\n */\nexport function Spinner({\n  state = \"loading\",\n  size: sizeProp,\n  label: labelProp,\n  delay = 0,\n  className,\n  classNames,\n  style,\n  ref,\n}: SpinnerProps) {\n  const i18n = useMessages(\"spinner\", englishMessages);\n  const label = labelProp === undefined ? i18n(\"label\") : labelProp;\n\n  const scopeDefaults = useComponentDefaults<SpinnerDefaults>(\"spinner\", [\"size\"]);\n  const size = sizeProp ?? scopeDefaults.size ?? \"md\";\n  const host = useRef<HTMLSpanElement>(null);\n  const attach = useComposedRef(host, ref);\n  const active = useActive(host);\n  const tokens = useMotionTokens(host);\n  const managedReduced = useReducedMotionSafe();\n  const reduced = tokens.reduced || managedReduced;\n\n  const box = PX[size];\n  const done = state !== \"loading\";\n  const tone = state === \"success\" ? \"text-green\" : state === \"error\" ? \"text-red\" : \"text-ink-2\";\n  return (\n    <span\n      ref={attach}\n      data-slot=\"spinner\"\n      data-state={state}\n      data-delay={delay > 0 && state === \"loading\" ? \"\" : undefined}\n      role={done ? \"img\" : \"status\"}\n      aria-label={state === \"success\" ? i18n(\"done\") : state === \"error\" ? i18n(\"error\") : label}\n      className={cn(styles.root, \"relative inline-grid shrink-0 place-items-center\", tone, classNames?.root, className)}\n      style={{ width: box, height: box, \"--_delay\": `${delay}ms`, ...style } as CSSProperties}\n    >\n      <svg\n        viewBox=\"0 0 24 24\"\n        width=\"100%\"\n        height=\"100%\"\n        fill=\"none\"\n        aria-hidden=\"true\"\n        data-state={state}\n        data-slot=\"spinner-svg\"\n        data-reduced={reduced ? \"\" : undefined}\n        data-paused={active ? undefined : \"\"}\n        className={cn(styles.svg, classNames?.svg)}\n      >\n        <circle\n          data-slot=\"spinner-track\"\n          className={cn(styles.track, classNames?.track)}\n          cx=\"12\"\n          cy=\"12\"\n          r=\"9.5\"\n          stroke=\"currentColor\"\n          strokeWidth=\"2.5\"\n        />\n        <circle\n          data-slot=\"spinner-arc\"\n          className={cn(styles.arc, classNames?.arc)}\n          cx=\"12\"\n          cy=\"12\"\n          r=\"9.5\"\n          pathLength={1}\n          stroke=\"currentColor\"\n          strokeWidth=\"2.5\"\n          strokeLinecap=\"round\"\n          transform=\"rotate(-90 12 12)\"\n        />\n        {state === \"success\" && (\n          <path\n            data-slot=\"spinner-mark\"\n            className={cn(styles.mark, classNames?.mark)}\n            d=\"M7.5 12.5l3 3 6-6.5\"\n            pathLength={1}\n            stroke=\"currentColor\"\n            strokeWidth=\"2.5\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n          />\n        )}\n        {state === \"error\" && (\n          <path\n            data-slot=\"spinner-mark\"\n            className={cn(styles.mark, classNames?.mark)}\n            d=\"M8.5 8.5l7 7M15.5 8.5l-7 7\"\n            pathLength={1}\n            stroke=\"currentColor\"\n            strokeWidth=\"2.5\"\n            strokeLinecap=\"round\"\n          />\n        )}\n      </svg>\n    </span>\n  );\n}\n\nexport default Spinner;\n"},{"path":"beamline/hooks/use-active.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-active.ts","content":"\"use client\";\n\nimport { useContext, useEffect, useState, type RefObject } from \"react\";\n\nimport { ActivityContext } from \"../runtime/activity\";\n\n/**\n * Whether an animated element should be doing work: it is (nearly) on screen and the tab is visible. Every\n * continuously animated component gates its loop, shader or CSS animation on this, so an effect scrolled out of view,\n * behind another tab, or in a collapsed panel costs nothing. One IntersectionObserver per element; no polling.\n */\nexport function useActive(\n  ref: RefObject<Element | null>,\n  {\n    margin = \"120px\",\n    key,\n    idle = true,\n  }: {\n    margin?: string;\n    /** Re-attach when this changes (an element that mounts later). */\n    key?: unknown;\n    /** Pause continuous work at the host idle boundary. Finite sequences set false so they finish. */\n    idle?: boolean;\n  } = {},\n) {\n  const active = useContext(ActivityContext);\n  const [inView, setInView] = useState(false);\n  const [visible, setVisible] = useState(\n    () => typeof document === \"undefined\" || document.visibilityState === \"visible\",\n  );\n  useEffect(() => {\n    const el = ref.current;\n    if (!el || typeof IntersectionObserver === \"undefined\") {\n      setInView(true);\n      return;\n    }\n    const io = new IntersectionObserver(([e]) => setInView(e.isIntersecting), { rootMargin: margin });\n    io.observe(el);\n    return () => io.disconnect();\n  }, [ref, margin, key]);\n  useEffect(() => {\n    const on = () => setVisible(document.visibilityState === \"visible\");\n    document.addEventListener(\"visibilitychange\", on);\n    return () => document.removeEventListener(\"visibilitychange\", on);\n  }, []);\n  return (!idle || active) && inView && visible;\n}\n"},{"path":"beamline/hooks/use-composed-ref.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-composed-ref.ts","content":"\"use client\";\n\nimport { useCallback, type Ref, type RefObject } from \"react\";\n\n/** Keep a control's measurement/focus ref and the caller's ref, including React 19 callback cleanup. */\nexport function useComposedRef<T>(internal: RefObject<T | null>, external?: Ref<T>) {\n  return useCallback(\n    (node: T | null) => {\n      internal.current = node;\n      if (typeof external === \"function\") {\n        const cleanup = external(node);\n        if (typeof cleanup === \"function\")\n          return () => {\n            internal.current = null;\n            cleanup();\n          };\n      } else if (external) external.current = node;\n    },\n    [internal, external],\n  );\n}\n"},{"path":"beamline/hooks/use-content-width.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-content-width.ts","content":"\"use client\";\n\nimport { useLayoutEffect, useRef, useState, type RefObject } from \"react\";\n\nconst same = (width: number) => width;\n\n/**\n * The width of an element's content box, kept current with a ResizeObserver. It is first read in a layout effect, so\n * a layout chosen by width (a narrow mode, a chart's scale) is right in the first frame instead of jumping a frame\n * later. `fallback` stands in on the server and until the element exists; `key` re-measures when the element is\n * replaced (one that exists only in one view).\n */\nexport function useContentWidth(ref: RefObject<HTMLElement | null>, fallback: number, key?: unknown): number {\n  return useWidthAs(ref, same, fallback, key);\n}\n\n/**\n * The same width, kept as what a part needs from it (`(w) => w < 640` for a narrow mode): the part renders again only\n * when that changes, not at every pixel.\n */\nexport function useWidthAs<T>(\n  ref: RefObject<HTMLElement | null>,\n  read: (width: number) => T,\n  fallback: T,\n  key?: unknown,\n): T {\n  const [value, setValue] = useState(fallback);\n  const latest = useRef(read);\n  latest.current = read;\n  useLayoutEffect(() => {\n    const el = ref.current;\n    if (!el) return;\n    // the computed width is fractional and ignores transforms, like the observer's content box\n    const s = getComputedStyle(el);\n    const edges = [\"paddingLeft\", \"paddingRight\", \"borderLeftWidth\", \"borderRightWidth\"] as const;\n    const box = parseFloat(s.width);\n    const outer = Number.isFinite(box) ? box : el.offsetWidth;\n    const inner =\n      Number.isFinite(box) && s.boxSizing !== \"border-box\" ? 0 : edges.reduce((w, k) => w + parseFloat(s[k]), 0);\n    setValue(latest.current(outer - inner));\n    if (typeof ResizeObserver === \"undefined\") return;\n    const ro = new ResizeObserver(([e]) => setValue(latest.current(e.contentRect.width)));\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, [ref, key]);\n  return value;\n}\n\n/**\n * The height of an element's content box, kept current like `useContentWidth`: for parts that lay out at whatever\n * height their parent gives them (a chart, a map or a book that fills its panel). Read in a layout effect, so the first\n * frame is right; with `enabled` false nothing is measured and the fallback stands.\n */\nexport function useContentHeight(\n  ref: RefObject<HTMLElement | null>,\n  fallback: number,\n  key?: unknown,\n  enabled = true,\n): number {\n  const [value, setValue] = useState(fallback);\n  useLayoutEffect(() => {\n    const el = ref.current;\n    if (!enabled || !el) return;\n    const s = getComputedStyle(el);\n    setValue(el.clientHeight - (parseFloat(s.paddingTop) || 0) - (parseFloat(s.paddingBottom) || 0));\n    if (typeof ResizeObserver === \"undefined\") return;\n    const ro = new ResizeObserver(([e]) => setValue(e.contentRect.height));\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, [ref, key, enabled]);\n  return enabled ? value : fallback;\n}\n"},{"path":"beamline/hooks/use-controllable.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-controllable.ts","content":"\"use client\";\n\nimport { useCallback, useRef, useState } from \"react\";\n\n/**\n * One value in both forms (rules/CONVENTIONS.md): the prop while it is passed, otherwise the element's own copy that\n * starts at `initial` (a value, or a function that makes it once). The setter takes a value or an updater; an updater\n * reads the latest value even twice in one event, and every change reaches `onChange`, controlled or not.\n * `Next` is what a change can carry when it is narrower than the state: a range that starts empty (null) but is only\n * ever changed to a range.\n */\nexport function useControllable<T, Next extends T = T>(\n  value: T | undefined,\n  initial: T | (() => T),\n  onChange?: (next: Next) => void,\n) {\n  const [own, setOwn] = useState<T>(initial);\n  const current = value !== undefined ? value : own;\n  const latest = useRef(current);\n  latest.current = current;\n  const set = useCallback(\n    (next: Next | ((was: T) => Next)) => {\n      const resolved = typeof next === \"function\" ? (next as (was: T) => Next)(latest.current) : next;\n      latest.current = resolved;\n      if (value === undefined) setOwn(resolved);\n      onChange?.(resolved);\n    },\n    [value, onChange],\n  );\n  return [current, set] as const;\n}\n"},{"path":"beamline/hooks/use-flip.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-flip.ts","content":"\"use client\";\n\nimport { useLayoutEffect, useRef, type RefObject } from \"react\";\nimport { useMotionTokens } from \"./use-motion-tokens\";\nimport { cssEase } from \"../lib/motion-tokens\";\n\ntype Seen = { el: HTMLElement; x: number; y: number; w: number; h: number };\n\n/**\n * Items of a collection keep their path (the morph grammar's \"changes keep their size and their path\"): mark each item\n * with `data-flip=\"<stable key>\"` inside `container`. After a change, an item that is new grows in, items that moved\n * slide from where they were, and an item that left fades out as a ghost where it stood (a still copy, inert and\n * hidden from assistive tech, removed when it has faded). The data and focus change at once; only the picture\n * settles. Nothing runs at rest; reduced motion places at once. Positions are kept in the container's own content\n * (relative to it, plus its scroll) and read at rest, so neither a scroll of the page, nor a scroll of the container\n * itself, nor an item still settling reads as movement.\n *\n * `resize`: an item whose box changed size also scales from its old box (for marks without text, a bar or a segment, so a\n * new value glides on transforms instead of laying out every frame). `origin(key)`: an element a new item grows out of\n * (a ribbon's segment becoming a column, a shared chip dividing into pieces); with `originFirst` it applies on the\n * container's first render too, so a panel that opens unfolds its items from what was already on screen. `inner`: items\n * inside another item that lies outside the container (a merge's rows inside a card that moves itself) are this\n * container's own; nesting is judged inside the container only.\n */\nexport function useFlip(\n  container: RefObject<HTMLElement | null> | (() => HTMLElement | null),\n  {\n    ghost = true,\n    enter = true,\n    resize = false,\n    origin,\n    originFirst = false,\n    inner = false,\n  }: {\n    ghost?: boolean;\n    enter?: boolean;\n    resize?: boolean;\n    origin?: (key: string) => Element | null | undefined;\n    originFirst?: boolean;\n    inner?: boolean;\n  } = {},\n) {\n  const get = typeof container === \"function\" ? container : () => container.current;\n  // timing read at the container itself, found when the commit has attached it (a function container has no ref of its\n  // own; without a host the timing stayed unresolved where no UIRoot is around, then rendered again to say so)\n  const host =\n    typeof container === \"function\"\n      ? {\n          get current() {\n            return container();\n          },\n        }\n      : container;\n  const motion = useMotionTokens(host);\n  const last = useRef<Map<string, Seen> | null>(null);\n  const lastWidth = useRef(0);\n  const flying = useRef(new Set<Animation>());\n  // where every item stands, in the container's own content (relative to it, plus its scroll): a list scrolled since the\n  // last render has moved nothing (a row that slid back by the scroll would leave the finger that is tapping it). The\n  // items' own grow-ins and slides still in flight are lifted while they are read, so each box is where the layout puts\n  // it: a render mid-flight (a live price ticking while a new row grows in) is not a move.\n  const read = (root: HTMLElement) => {\n    const lifted = [...flying.current].map((a) => a.effect as KeyframeEffect).map((e) => [e, e.target] as const);\n    for (const [effect] of lifted) effect.target = null;\n    const box = root.getBoundingClientRect();\n    const now = new Map<string, Seen>();\n    for (const el of root.querySelectorAll<HTMLElement>(\"[data-flip]\")) {\n      // only the outermost items move (with `inner`, outermost inside this container)\n      const outer = el.parentElement?.closest(\"[data-flip]\");\n      if (outer && (!inner || (outer !== root && root.contains(outer)))) continue;\n      const r = el.getBoundingClientRect();\n      now.set(el.dataset.flip!, {\n        el,\n        x: r.left - box.left + root.scrollLeft,\n        y: r.top - box.top + root.scrollTop,\n        w: r.width,\n        h: r.height,\n      });\n    }\n    for (const [effect, target] of lifted) effect.target = target;\n    return { box, now };\n  };\n  // a container that changes size at rest, with no render (fonts arriving, a scrollbar appearing, a window resized), has\n  // its places read again then: else the first change after it would take the new width for a reflow and move nothing\n  const watched = useRef<{ el: HTMLElement; observer: ResizeObserver } | null>(null);\n  useLayoutEffect(() => () => watched.current?.observer.disconnect(), []);\n  // every render on purpose: a collection changes through its parent's state, not through anything we can depend on\n  useLayoutEffect(() => {\n    const root = get();\n    // a container that is gone (a panel folded) starts over when it comes back: its items are new, not moved\n    if (!root) {\n      last.current = null;\n      return;\n    }\n    if (watched.current?.el !== root && typeof ResizeObserver !== \"undefined\") {\n      watched.current?.observer.disconnect();\n      const observer = new ResizeObserver(() => {\n        if (flying.current.size || !last.current || !root.isConnected) return;\n        const seen = read(root);\n        last.current = seen.now;\n        lastWidth.current = seen.box.width;\n      });\n      observer.observe(root);\n      watched.current = { el: root, observer };\n    }\n    const { box, now } = read(root);\n    // a container that changed width since the last render reflowed its items by itself (a panel resized, a room\n    // step crossed): that is placement, not a change a person made, so this render records the new places and moves\n    // nothing (a reflow places, only a choice travels)\n    const reflowed = Math.abs(box.width - lastWidth.current) > 0.5;\n    lastWidth.current = box.width;\n    const before = reflowed ? null : last.current;\n    last.current = now;\n    const ms = motion.duration.standard * 1000;\n    // the first render moves nothing, except items that grow out of an origin already on screen\n    if ((!before && !(origin && originFirst)) || motion.reduced || ms < 2) return;\n    const easing = cssEase(motion.ease.standard, \"ease-out\");\n    // an item in flight says so (`data-flip-flight`: \"far\" when it crosses more than its own size, a slide between groups or\n    // a share growing out of another item; \"near\" for a neighbour's step), so a part can lift what travels over other\n    // text onto a surface of its own instead of drawing words over words\n    const fly = (el: HTMLElement, keyframes: Keyframe[], far = false) => {\n      const flight = el.animate(keyframes, { duration: ms, easing });\n      flying.current.add(flight);\n      if (far || el.dataset.flipFlight !== \"far\") el.dataset.flipFlight = far ? \"far\" : \"near\";\n      const land = () => {\n        flying.current.delete(flight);\n        if (![...flying.current].some((a) => (a.effect as KeyframeEffect | null)?.target === el))\n          delete el.dataset.flipFlight;\n      };\n      flight.finished.then(land, land);\n    };\n    const boxOf = (key: string) => {\n      const from = origin?.(key);\n      if (!from || !from.isConnected) return null;\n      const r = from.getBoundingClientRect();\n      return { x: r.left - box.left + root.scrollLeft, y: r.top - box.top + root.scrollTop, w: r.width, h: r.height };\n    };\n    // from a box to where the item stands: translate and scale about the top-left corner\n    const morph = (was: { x: number; y: number; w: number; h: number }, at: Seen): Keyframe[] => [\n      {\n        transformOrigin: \"0 0\",\n        transform: `translate(${was.x - at.x}px, ${was.y - at.y}px) scale(${was.w / Math.max(at.w, 0.5)}, ${was.h / Math.max(at.h, 0.5)})`,\n      },\n      { transformOrigin: \"0 0\", transform: \"none\" },\n    ];\n    for (const [key, at] of now) {\n      const was = before?.get(key);\n      if (!was) {\n        const from = boxOf(key);\n        if (from) fly(at.el, morph(from, at), true);\n        else if (enter && before)\n          fly(at.el, [\n            { opacity: 0, transform: \"scale(0.86)\" },\n            { opacity: 1, transform: \"none\" },\n          ]);\n        continue;\n      }\n      const dx = was.x - at.x,\n        dy = was.y - at.y;\n      const resized = resize && (Math.abs(was.w - at.w) > 0.5 || Math.abs(was.h - at.h) > 0.5);\n      const far = Math.abs(dx) > at.w * 0.5 || Math.abs(dy) > at.h * 1.5;\n      if (resized) fly(at.el, morph(was, at), far);\n      else if (Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5)\n        fly(at.el, [{ transform: `translate(${dx}px, ${dy}px)` }, { transform: \"none\" }], far);\n    }\n    if (!ghost || !before) return;\n    for (const [key, was] of before) {\n      if (now.has(key) || was.el.isConnected) continue;\n      const copy = was.el.cloneNode(true) as HTMLElement;\n      copy.removeAttribute(\"data-flip\");\n      // the ghost is a picture of the item, not the item: nothing that finds parts by slot or id may find it\n      for (const n of [copy, ...copy.querySelectorAll(\"[id], [data-slot]\")]) {\n        n.removeAttribute(\"id\");\n        n.removeAttribute(\"data-slot\");\n      }\n      copy.setAttribute(\"data-ghost\", \"\");\n      copy.setAttribute(\"aria-hidden\", \"true\");\n      copy.inert = true;\n      if (getComputedStyle(root).position === \"static\") root.style.position = \"relative\";\n      Object.assign(copy.style, {\n        position: \"absolute\",\n        left: `${was.x}px`,\n        top: `${was.y}px`,\n        width: `${was.w}px`,\n        height: `${was.h}px`,\n        margin: \"0\",\n        pointerEvents: \"none\",\n      });\n      root.appendChild(copy);\n      // the ghost is gone by the time a neighbour slides into its place, so their texts never mix. The clock runs\n      // straight and an even curve shapes only the fade: the front-loaded standard curve on the whole timeline passed\n      // 0.45 inside the first frame, so what left read as a pop instead of a fade\n      const fade = copy.animate(\n        [\n          { opacity: 1, transform: \"none\", easing: cssEase(motion.ease.default, \"ease-in-out\") },\n          { opacity: 0, transform: \"scale(0.9)\", offset: 0.6 },\n          { opacity: 0, transform: \"scale(0.86)\" },\n        ],\n        {\n          duration: motion.duration.standard * 1000,\n          easing: \"linear\",\n        },\n      );\n      const drop = () => copy.remove();\n      fade.finished.then(drop, drop);\n      // and by the clock as well: a fade that never reports its end (the page hidden, an animation cancelled from\n      // outside) must not leave a copy behind for a person or a test to find\n      setTimeout(drop, motion.duration.standard * 1000 + 250);\n    }\n  });\n}\n"},{"path":"beamline/hooks/use-media-query.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-media-query.ts","content":"\"use client\";\n\nimport { useSyncExternalStore } from \"react\";\n\n// one MediaQueryList per query: React reads the snapshot on every render, and a part that renders thirty times a\n// second (a playing audio player's clock) would otherwise ask the browser for a new list each time\nconst lists = new Map<string, MediaQueryList>();\nconst list = (query: string) => {\n  if (typeof window === \"undefined\" || typeof window.matchMedia !== \"function\") return undefined;\n  let m = lists.get(query);\n  if (!m) lists.set(query, (m = window.matchMedia(query)));\n  return m;\n};\n\n/** True while the media query matches; false on the server and in the first client render of SSR output. */\nexport function useMediaQuery(query: string): boolean {\n  return useSyncExternalStore(\n    (notify) => {\n      const m = list(query);\n      m?.addEventListener(\"change\", notify);\n      return () => m?.removeEventListener(\"change\", notify);\n    },\n    () => list(query)?.matches ?? false,\n    () => false,\n  );\n}\n\n/** Phone-width layouts: below 768px. */\nexport const usePhone = () => useMediaQuery(\"(max-width: 767px)\");\n"},{"path":"beamline/hooks/use-motion-tokens.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-motion-tokens.ts","content":"\"use client\";\n\nimport { useMemo, type RefObject } from \"react\";\nimport { useAppearanceRoot } from \"../runtime/appearance\";\nimport { useResolvedAppearance } from \"./use-resolved-appearance\";\nimport { motionAt, motionEase, motionTokens, type MotionTokens } from \"../lib/motion-tokens\";\nimport { semanticMotion } from \"../lib/motion-source\";\n\nconst durationToken = (name: string) =>\n  name === \"exit\" ? \"motion-out\" : name === \"in\" || name === \"out\" ? `motion-${name}` : `duration-${name}`;\nconst easeToken = (name: string) => `ease-${name === \"inOut\" ? \"in-out\" : name}`;\nconst REQUEST = {\n  times: [\n    ...Object.keys(semanticMotion.duration).map(durationToken),\n    ...Object.keys(semanticMotion.spring).map((name) => `spring-${name}`),\n    ...Object.keys(semanticMotion.stagger).map((name) => `stagger-${name}`),\n    \"fade-in\",\n    \"fade-out\",\n  ],\n  easings: [...Object.keys(semanticMotion.ease).map(easeToken), \"ease-spring\", \"ease-spring-bounce\", \"ease-overshoot\"],\n  numbers: [\"motion-scale\", \"motion-look\", \"press-scale\"],\n  // timing only: the host's own type would make every control resolve differently from the page around it\n  typography: false,\n};\n\n/**\n * Resolve each timing at the actual host; omitted host uses the nearest managed root. The first/server snapshot is\n * the pure foundation default. Local CSS values, scope changes, font readiness, OS preference and explicit refresh\n * update the same consumer; no document-wide first-root fallback or persistent global listener.\n */\nexport function useMotionTokens(hostRef?: RefObject<HTMLElement | null>): MotionTokens & {\n  ready: boolean;\n  /** Reduced or none: movement, springs and staggers are instant. */\n  reduced: boolean;\n  mode: \"full\" | \"reduced\" | \"none\";\n  /** Opacity-only arrival and departure (seconds): kept under reduced, instant under none (APPEARANCE A06). */\n  fade: { in: number; out: number };\n} {\n  const root = useAppearanceRoot();\n  const scopeRef = useMemo(() => ({ current: root }), [root]);\n  const resolved = useResolvedAppearance(hostRef ?? scopeRef, REQUEST);\n  return useMemo(() => {\n    if (!resolved.ready)\n      return {\n        ...motionTokens,\n        ready: false,\n        reduced: false,\n        mode: \"full\" as const,\n        fade: { in: motionTokens.duration.in, out: motionTokens.duration.out },\n      };\n    const reduced = resolved.reducedMotion;\n    const speed = (resolved.numbers[\"motion-scale\"] ?? 1) * (resolved.numbers[\"motion-look\"] ?? 1);\n    const fade =\n      resolved.motionMode === \"none\"\n        ? { in: 0.001, out: 0.001 }\n        : {\n            in: resolved.times[\"fade-in\"] ?? semanticMotion.duration.in * speed,\n            out: resolved.times[\"fade-out\"] ?? semanticMotion.duration.out * speed,\n          };\n    const factor = reduced ? 0.001 / semanticMotion.duration.standard : speed;\n    const base = motionAt(factor);\n    const duration = Object.fromEntries(\n      Object.entries(base.duration).map(([name, fallback]) => [\n        name,\n        reduced ? 0.001 : (resolved.times[durationToken(name)] ?? fallback),\n      ]),\n    ) as MotionTokens[\"duration\"];\n    const stagger = Object.fromEntries(\n      Object.entries(base.stagger).map(([name, fallback]) => [\n        name,\n        reduced ? 0 : (resolved.times[`stagger-${name}`] ?? fallback),\n      ]),\n    ) as MotionTokens[\"stagger\"];\n    const ease = Object.fromEntries(\n      Object.entries(base.ease).map(([name, fallback]) => [\n        name,\n        motionEase(resolved.easings[easeToken(name)], fallback),\n      ]),\n    ) as MotionTokens[\"ease\"];\n    const spring = { ...base.spring };\n    for (const name of Object.keys(semanticMotion.spring) as (keyof typeof semanticMotion.spring)[])\n      spring[name] = {\n        ...base.spring[name],\n        visualDuration: reduced ? 0.001 : (resolved.times[`spring-${name}`] ?? base.spring[name].visualDuration),\n        bounce: reduced ? 0 : base.spring[name].bounce,\n      };\n    return {\n      ...base,\n      ready: true,\n      reduced,\n      mode: resolved.motionMode,\n      fade,\n      duration,\n      stagger,\n      ease,\n      spring,\n      pressScale: resolved.numbers[\"press-scale\"] ?? base.pressScale,\n      css: {\n        spring: resolved.easings[\"ease-spring\"] ?? base.css.spring,\n        springBounce: resolved.easings[\"ease-spring-bounce\"] ?? base.css.springBounce,\n        overshoot: resolved.easings[\"ease-overshoot\"] ?? base.css.overshoot,\n      },\n    };\n  }, [resolved]);\n}\n"},{"path":"beamline/hooks/use-reduced-motion-safe.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-reduced-motion-safe.ts","content":"\"use client\";\n\nimport { useMediaQuery } from \"./use-media-query\";\nimport { useScopeMotion } from \"../runtime/appearance\";\n\n/** Managed motion policy plus the OS preference, with a stable server snapshot. */\nexport function useReducedMotionSafe() {\n  const requested = useScopeMotion();\n  const os = useMediaQuery(\"(prefers-reduced-motion: reduce)\");\n  return requested !== \"auto\" || os;\n}\n"},{"path":"beamline/lib/now.ts","type":"registry:file","target":"src/ui/beamline/lib/now.ts","content":"import { useContext, useLayoutEffect, useMemo, useState, useSyncExternalStore } from \"react\";\nimport { ActivityContext } from \"../runtime/activity\";\n\n/*\n * The time, read only in the browser. A component that reads the clock while it renders breaks server rendering:\n * Next.js refuses to prerender a page that does (\"encountered the unstable value `new Date()` in a Client Component\",\n * create-next-app's default since 16), and any server's time differs from the browser's at hydration. These hooks give\n * null on the server and while hydrating, and the real value in the browser before the first paint.\n */\n\nconst still = () => () => {};\nconst startOfToday = () => {\n  const d = new Date();\n  return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();\n};\nconst onServer = () => null;\n\n/** Today at 00:00 local time; null on the server and while hydrating. */\nexport function useToday(): Date | null {\n  const day = useSyncExternalStore(still, startOfToday, onServer);\n  return useMemo(() => (day === null ? null : new Date(day)), [day]);\n}\n\n/** The time in ms, read before the first paint and again every `every` ms while `ticking`; null on the server. */\nexport function useNow(ticking = false, every = 1000, until?: number): number | null {\n  const active = useContext(ActivityContext);\n  const [now, setNow] = useState<number | null>(null);\n  // A finite deadline must reach its completion callback even when the host is idle, then stop ticking.\n  const running = ticking && (until === undefined ? active : now === null || now < until);\n  useLayoutEffect(() => {\n    setNow(Date.now());\n    if (!running) return;\n    const id = setInterval(() => setNow(Date.now()), every);\n    return () => clearInterval(id);\n  }, [running, every, until]);\n  return now;\n}\n"},{"path":"beamline/lib/utils.ts","type":"registry:file","target":"src/ui/beamline/lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\";\nimport { extendTailwindMerge } from \"tailwind-merge\";\n\n/* The type roles (utilities.css) and the 2xs step are sizes, not text colours: without this, tailwind-merge reads\n   `text-overline` as a colour and drops it next to `text-ink-3`. A role and a size step still replace each other.\n   Likewise `font-title` is the look's title weight, not a family: read as a family it dropped `font-display` beside it,\n   and every merged page title lost the look's display face. */\nconst twMerge = extendTailwindMerge({\n  extend: {\n    classGroups: {\n      \"font-weight\": [{ font: [\"title\"] }],\n      \"font-family\": [{ font: [\"display\", \"numeric\", \"figure\"] }],\n      \"font-size\": [\n        {\n          text: [\n            \"2xs\",\n            \"display\",\n            \"title\",\n            \"heading\",\n            \"subheading\",\n            \"body\",\n            \"body-sm\",\n            \"reading\",\n            \"label\",\n            \"caption\",\n            \"overline\",\n            \"numeric\",\n            \"code\",\n          ],\n        },\n      ],\n    },\n  },\n});\n\n/** Merge class names, letting later Tailwind utilities win over earlier ones. */\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n\n/** A small seeded random generator (LCG): the same seed gives the same sequence, for stable sample data and jitter. */\nexport function rng(seed = 1) {\n  let s = seed >>> 0 || 1;\n  return () => (s = (s * 1664525 + 1013904223) >>> 0) / 2 ** 32;\n}\n\nexport const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));\n"}],"docs":"Run trace is in src/ui/beamline/components/run-trace/run-trace.tsx. The source is in src/ui/beamline (ui/beamline in a project without src/). After Tailwind, import its styles/beamline.css once, then styles/look-prism.css in your app stylesheet (use relative paths). Keep the shared base before all chosen looks. Wrap your app in <UIRoot preset={prism}> with UIRoot from its runtime/appearance and prism from its looks/prism.  shadcn replaces files when asked; use the Beamline managed-source CLI from the start for three-way upgrades that preserve edits.","meta":{"free":true,"version":"0.2.0-rc.42","library":"https://beamline.io/components/run-trace"}}