{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"reasoning","type":"registry:component","title":"Reasoning","description":"The agent's trace, fed by your stream: a shimmering label and the seconds so far while it works, then \"Thought for N seconds\"; steps that check off, prose thoughts, search sources or tool calls, open while running and folded when done.","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/_shared/row-action.module.css","type":"registry:file","target":"src/ui/beamline/components/_shared/row-action.module.css","content":"@layer properties, theme, base, components, utilities;\n/* A row of a list that you press as a whole: a ticket in an inbox, a service on a status page, a key in a table, a\n   disclosure header. The caller lays out its content (grid or flex, with utilities); the row owns how it answers:\n   the hover plate arrives at once and leaves over --pui-motion-out, a press deepens the plate (a wide row never\n   scales), the focus ring sits inside its edge so a clipped list never cuts it, and the current row wears\n   .row-current. */\n@layer components {\n  .root {\n    inline-size: 100%;\n    min-inline-size: 0;\n    border-radius: var(--pui-radius-control);\n    text-align: start;\n    outline-offset: calc(-1 * var(--pui-focus-width));\n    transition:\n      background-color var(--pui-motion-out) var(--pui-ease-default),\n      color var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .root:disabled {\n    cursor: default;\n  }\n  .root[data-plate]:hover:not(:disabled) {\n    background-color: var(--pui-hover);\n    transition-duration: 0ms;\n  }\n  .root[data-plate]:active:not(:disabled) {\n    background-color: var(--pui-hover-2);\n    transition-duration: 0ms;\n  }\n  /* neutral current: an ink edge on a quiet plate, for a side list whose choice is only \"the one open\" */\n  .root[data-current][data-tone=\"neutral\"] {\n    background-color: var(--pui-hover-2);\n    box-shadow: inset 2px 0 0 var(--pui-ink-2);\n  }\n  .root[data-current][data-tone=\"neutral\"]:dir(rtl) {\n    box-shadow: inset -2px 0 0 var(--pui-ink-2);\n  }\n}\n"},{"path":"beamline/components/_shared/row-action.tsx","type":"registry:file","target":"src/ui/beamline/components/_shared/row-action.tsx","content":"\"use client\";\n// organism-allow-file: raw-control -- the shared part every pressable row is drawn from\n\nimport type { ButtonHTMLAttributes, Ref } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./row-action.module.css\";\n\nexport interface RowActionProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n  /** The one row open beside the list: the current tint and edge (.row-current; the accent, or the look's own where\n   *  its accent is a status hue, as in pop). Say which with aria-current or\n   *  aria-pressed as well; colour is never the only signal. */\n  current?: boolean;\n  /** How the current row is painted: the accent (default), or neutral (an ink edge on a quiet plate) for a choice in a\n   *  side list that is only \"the one open\", where a look's accent (red in Pop) would read as an alarm. */\n  tone?: \"accent\" | \"neutral\";\n  /** The hover plate; off for a row whose list already draws its own (a gliding indicator, an inset card). */\n  plate?: boolean;\n  ref?: Ref<HTMLButtonElement>;\n}\n\n/** A row pressed as a whole (a list item, a table's record, a disclosure header); its content is the caller's layout. */\nexport function RowAction({\n  current,\n  tone = \"accent\",\n  plate = true,\n  type = \"button\",\n  className,\n  ref,\n  ...props\n}: RowActionProps) {\n  return (\n    <button\n      ref={ref}\n      type={type}\n      data-plate={plate ? \"\" : undefined}\n      data-current={current ? \"\" : undefined}\n      data-tone={tone === \"neutral\" ? \"neutral\" : undefined}\n      className={cn(styles.root, current && tone !== \"neutral\" && \"row-current\", className)}\n      {...props}\n    />\n  );\n}\n"},{"path":"beamline/components/reasoning/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/reasoning/defaults.ts","content":"/** Pure allowed presentation defaults for Reasoning; no component or runtime imports. */\nexport interface ReasoningDefaults {\n  /** Shared mode setting; omit to inherit the component default. Scoped defaults retain the legacy key variant. */\n  variant?: \"steps\" | \"reasoning\" | \"search\" | \"coding\";\n}\n"},{"path":"beamline/components/reasoning/messages.ts","type":"registry:file","target":"src/ui/beamline/components/reasoning/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  activeLabel: \"Thinking\",\n  s: \"s\",\n  finishedAfterSeconds: {\n    one: \"Finished after {value} second\",\n    other: \"Finished after {value} seconds\",\n  },\n  trace: \"Trace\",\n  searchedFor: \"Searched for\",\n  more: \"+{more} more\",\n  lines: \"lines\",\n  inProgress: \", in progress\",\n  done: \", done\",\n  thought: \"Thought\",\n  thoughtForSeconds: {\n    one: \"Thought for {count} second\",\n    other: \"Thought for {count} seconds\",\n  },\n  seconds: \", {elapsed} seconds\",\n  /** A folded, finished trace's count of its insides, after its label. */\n  summarySteps: {\n    one: \"{count} step\",\n    other: \"{count} steps\",\n  },\n  summarySources: {\n    one: \"{count} source\",\n    other: \"{count} sources\",\n  },\n  summaryCalls: {\n    one: \"{count} tool call\",\n    other: \"{count} tool calls\",\n  },\n  /** For screen readers, after the calls: the lines they changed. */\n  summaryDelta: \"{added} lines added, {removed} removed\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/reasoning/reasoning.tsx","type":"registry:file","target":"src/ui/beamline/components/reasoning/reasoning.tsx","content":"\"use client\";\n\nimport { useMessages } from \"../../runtime/localization\";\nimport { english as englishMessages } from \"./messages\";\nimport { useDeprecatedProp } from \"../../hooks/use-deprecated-prop\";\n\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { ReasoningDefaults } from \"./defaults\";\nexport type { ReasoningDefaults } from \"./defaults\";\n\nimport { useEffect, useLayoutEffect, useRef, useState, type CSSProperties, type Ref, type ReactNode } from \"react\";\nimport { Brain, Check, ChevronDown, Search } from \"lucide-react\";\nimport { useWidthMorph } from \"../../hooks/use-width-morph\";\nimport { useReducedMotionSafe } from \"../../hooks/use-reduced-motion-safe\";\nimport { Spinner } from \"../spinner/spinner\";\nimport { SlotText } from \"../slot-text/slot-text\";\nimport { useMotionTokens } from \"../../hooks/use-motion-tokens\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { TextShimmer } from \"../text-shimmer/text-shimmer\";\nimport { cn } from \"../../lib/utils\";\nimport { RowAction } from \"../_shared/row-action\";\nimport { useControllable } from \"../../hooks/use-controllable\";\nimport { useNow } from \"../../lib/now\";\n\n/** How the trace reads: a checklist of steps, prose thoughts, a search with its sources, or tool calls (reads, edits, commands). */\nexport type ReasoningVariant = \"steps\" | \"reasoning\" | \"search\" | \"coding\";\n\nexport interface ReasoningRow {\n  /** Stable key; defaults to the row's position. */\n  id?: string;\n  /** Main text displayed for this reasoning row. Required. */\n  primary: string;\n  /** Secondary text displayed for this reasoning row. Omitted by default. */\n  secondary?: string;\n  /** Monospace secondary text (file names, commands). */\n  mono?: boolean;\n  /** Lines added and removed, for edits. */\n  add?: number;\n  /** Number of lines deleted by this coding step. Omitted by default. */\n  del?: number;\n  /** Source link, for search rows. */\n  href?: string;\n}\n\nexport type ReasoningClassNames = Partial<\n  Record<\n    | \"root\"\n    | \"trigger\"\n    | \"icon\"\n    | \"heading\"\n    | \"label\"\n    | \"elapsed\"\n    | \"summary\"\n    | \"chevron\"\n    | \"disclosure\"\n    | \"clip\"\n    | \"trace\"\n    | \"query\"\n    | \"more\"\n    | \"row\"\n    | \"rowContent\"\n    | \"primary\"\n    | \"secondary\"\n    | \"delta\",\n    string\n  >\n>;\n\nexport interface ReasoningProps {\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?: ReasoningClassNames;\n  /** Receives the documented root element; no ref by default. */\n  ref?: Ref<HTMLDivElement>;\n  /** Reasoning steps, prose, sources or tool calls displayed in order. Required. */\n  rows: ReasoningRow[];\n  /** Shows steps, prose reasoning, search sources or coding calls; defaults to steps. */\n  mode?: ReasoningVariant;\n  /** Legacy mode setting; omitted by default. @deprecated Use mode instead. */\n  variant?: ReasoningVariant;\n  /** `running` says the label in a shimmer and spins the last step; `done` settles and folds the trace. */\n  status?: \"running\" | \"done\";\n  /** Header while running, e.g. \"Thinking\", \"Searching the web\". */\n  activeLabel?: string;\n  /** Header once done. Defaults to \"Thought for N seconds\", timed from when the run started. */\n  doneLabel?: string;\n  /** When the run started (ms since epoch). Defaults to the moment `status` first became `running`. */\n  startedAt?: number;\n  /** Search query shown above search rows. */\n  query?: string;\n  /** Count of further rows not listed (\"+7 more\"). */\n  more?: number;\n  /** The trace open, controlled or not. Uncontrolled it is open while running and folds when done, until the person toggles it. */\n  open?: boolean;\n  /** Initial expansion override; omitted by default to open while running and fold after completion. */\n  defaultOpen?: boolean;\n  /** Reports open state after a change; no callback by default. */\n  onOpenChange?: (open: boolean) => void;\n  /** What a finished, folded trace says after its label, so the line tells what happened without opening it (\"ran 5\n   *  tests, 1 failed\"). Defaults to a count of its insides: steps, sources, or tool calls with the lines they added and\n   *  removed; prose has none. `false` shows nothing. Hidden while open, where the trace speaks for itself. */\n  summary?: ReactNode | false;\n  /** Header glyph; defaults to a brain. */\n  icon?: ReactNode;\n  /** Classes on the documented visual root; no override by default. */\n  className?: string;\n}\n\nconst host = (url?: string) => {\n  try {\n    return url ? new URL(url).hostname.replace(/^www\\./, \"\") : \"\";\n  } catch {\n    return \"\";\n  }\n};\n\n/** Whole seconds since the run started, ticking once a second while it runs; frozen when it ends. */\nfunction useElapsed(running: boolean, startedAt?: number) {\n  const start = useRef<number | null>(startedAt ?? null);\n  // the clock is read in the browser only (lib/now): a server render shows no seconds yet\n  const now = useNow(running);\n  if (startedAt !== undefined) start.current = startedAt;\n  // a run that starts later is timed from that moment, not from when the trace first showed\n  useLayoutEffect(() => {\n    if (running && start.current === null) start.current = Date.now();\n  }, [running]);\n  return start.current === null || now === null ? null : Math.max(0, Math.round((now - start.current) / 1000));\n}\n\n/**\n * The agent's trace, fed by your stream: what it is doing while it works (a shimmering label and the seconds so far),\n * then \"Thought for N seconds\" once it is done. The trace is open while the agent runs and folds itself away when it\n * finishes, unless the person opened or closed it; it can always be opened again.\n */\nexport function Reasoning({\n  rows,\n  mode: modeProp,\n  variant: variantProp,\n  status = \"done\",\n  activeLabel: activeLabelProp,\n  doneLabel,\n  startedAt,\n  query,\n  more,\n  open: openProp,\n  defaultOpen,\n  onOpenChange,\n  summary,\n  icon,\n  className,\n  classNames,\n  style,\n  ref,\n}: ReasoningProps) {\n  const i18n = useMessages(\"reasoning\", englishMessages);\n  const activeLabel = activeLabelProp === undefined ? i18n(\"activeLabel\") : activeLabelProp;\n\n  const scopeDefaults = useComponentDefaults<ReasoningDefaults>(\"reasoning\", [\"variant\"]);\n  useDeprecatedProp(\n    \"reasoning\",\n    \"variant\",\n    \"mode\",\n    variantProp !== undefined || (modeProp === undefined && scopeDefaults.variant !== undefined),\n  );\n  const variant = modeProp ?? variantProp ?? scopeDefaults.variant ?? \"steps\";\n\n  const running = status === \"running\";\n  const elapsed = useElapsed(running, startedAt);\n  // null hands the open state to the run (open while it runs); a new run hands it back without a report, so the\n  // hook holds the choice and a toggle reports beside it\n  const [chosen, setChosen] = useControllable<boolean | null>(openProp, defaultOpen ?? null);\n  const open = chosen ?? running;\n  const toggle = () => {\n    setChosen(!open);\n    onOpenChange?.(!open);\n  };\n\n  // A new run (the rows were cleared) hands the open state back to the run.\n  const count = useRef(rows.length);\n  useEffect(() => {\n    if (rows.length < count.current) setChosen(defaultOpen ?? null);\n    count.current = rows.length;\n  }, [rows.length, defaultOpen, setChosen]);\n\n  // say the result once a run this component saw has finished (not for history that mounts settled)\n  const ran = useRef(running);\n  if (running) ran.current = true;\n  const settled = doneLabel ?? (elapsed === null ? i18n(\"thought\") : i18n(\"thoughtForSeconds\", { count: elapsed }));\n  // folded and finished, the line counts what is inside (or says the app's own summary); open, the trace says it\n  const total = rows.length + (more ?? 0);\n  const added = rows.reduce((sum, row) => sum + (row.add ?? 0), 0);\n  const removed = rows.reduce((sum, row) => sum + (row.del ?? 0), 0);\n  const counted =\n    total === 0\n      ? null\n      : variant === \"coding\"\n        ? i18n(\"summaryCalls\", { count: total })\n        : variant === \"search\"\n          ? i18n(\"summarySources\", { count: total })\n          : variant === \"steps\"\n            ? i18n(\"summarySteps\", { count: total })\n            : null;\n  const delta = variant === \"coding\" && summary === undefined && (added > 0 || removed > 0);\n  const told = summary === false ? null : (summary ?? counted);\n  const showSummary = !running && !open && told !== null && told !== \"\";\n  const summaryWords =\n    typeof told === \"string\"\n      ? `${told}${delta ? `, ${i18n(\"summaryDelta\", { added: added, removed: removed })}` : \"\"}`\n      : \"\";\n  // the label, its seconds and the summary ease to each new width, so the chevron after them glides instead of jumping\n  const heading = useRef<HTMLSpanElement>(null);\n  useWidthMorph(heading, running ? `${activeLabel} ${elapsed}` : `${settled}${showSummary ? \"+\" : \"\"}`);\n  const root = useRef<HTMLDivElement>(null);\n  const attach = useComposedRef(root, ref);\n  const motion = useMotionTokens(root);\n  return (\n    <div\n      ref={attach}\n      style={style}\n      data-slot=\"reasoning\"\n      data-state={running ? \"running\" : \"done\"}\n      data-variant={variant}\n      className={cn(\"@container flex w-full max-w-lg flex-col\", classNames?.root, className)}\n    >\n      <RowAction\n        plate={false}\n        data-slot=\"reasoning-trigger\"\n        aria-expanded={open}\n        onClick={toggle}\n        aria-label={\n          running\n            ? `${activeLabel}${elapsed !== null ? i18n(\"seconds\", { elapsed: elapsed }) : \"\"}`\n            : showSummary && summaryWords\n              ? `${settled}, ${summaryWords}`\n              : settled\n        }\n        className={cn(\n          \"-mx-1.5 flex w-fit items-center gap-2 px-1.5 py-1 text-sm font-medium hover:bg-hover-2\",\n          classNames?.trigger,\n        )}\n      >\n        <span\n          data-slot=\"reasoning-icon\"\n          aria-hidden\n          className={cn(\"flex shrink-0 [&_svg]:size-4\", running ? \"text-ink-2\" : \"text-ink-3\", classNames?.icon)}\n        >\n          {icon ?? <Brain />}\n        </span>\n        <span\n          ref={heading}\n          data-slot=\"reasoning-heading\"\n          className={cn(\"flex items-center gap-2 overflow-clip whitespace-nowrap\", classNames?.heading)}\n        >\n          {running ? (\n            <TextShimmer className={cn(\"whitespace-nowrap\", classNames?.label)}>{activeLabel}</TextShimmer>\n          ) : (\n            // a run that ended here says so with a fade; history is simply there\n            <span\n              data-slot=\"reasoning-label\"\n              className={cn(\"whitespace-nowrap text-ink-2\", ran.current && \"animate-enter-fade\", classNames?.label)}\n            >\n              {settled}\n            </span>\n          )}\n          {showSummary && (\n            <span\n              data-slot=\"reasoning-summary\"\n              className={cn(\"flex animate-enter-fade items-center gap-1.5 font-normal text-ink-3\", classNames?.summary)}\n            >\n              <span aria-hidden>·</span>\n              {told}\n              {delta && (\n                <span className=\"font-numeric text-2xs tabular-nums\">\n                  <span className=\"text-up\">+{added}</span> <span className=\"text-down\">−{removed}</span>\n                </span>\n              )}\n            </span>\n          )}\n          {running && elapsed !== null && (\n            <span\n              data-slot=\"reasoning-elapsed\"\n              aria-hidden\n              className={cn(\"font-numeric text-2xs font-normal text-ink-3 tabular-nums\", classNames?.elapsed)}\n            >\n              <SlotText value={elapsed} duration={motion.duration.morph * 1000} stagger={0} />\n              {i18n(\"s\")}\n            </span>\n          )}\n        </span>\n        <ChevronDown\n          data-slot=\"reasoning-chevron\"\n          aria-hidden\n          className={cn(\n            \"size-3.5 text-ink-3 transition-transform duration-(--pui-motion-out)\",\n            open && \"rotate-180\",\n            classNames?.chevron,\n          )}\n        />\n      </RowAction>\n      <span aria-live=\"polite\" className=\"sr-only\">\n        {ran.current && !running ? i18n(\"finishedAfterSeconds\", { value: elapsed ?? 0, count: elapsed ?? 0 }) : \"\"}\n      </span>\n\n      <div\n        data-slot=\"reasoning-disclosure\"\n        className={cn(\"disclosure-panel\", classNames?.disclosure)}\n        data-open={open || undefined}\n      >\n        <div data-slot=\"reasoning-clip\" className={classNames?.clip}>\n          <ol\n            data-slot=\"reasoning-trace\"\n            className={cn(\n              \"ms-[calc(var(--pui-icon-sm)/2)] mt-1 flex flex-col gap-0.5 border-s border-line py-1 ps-3.5\",\n              classNames?.trace,\n            )}\n            aria-label={i18n(\"trace\")}\n          >\n            {query && (\n              <li\n                data-slot=\"reasoning-query\"\n                className={cn(\"flex min-h-6 items-center gap-2 text-xs text-ink-2\", classNames?.query)}\n              >\n                <Search aria-hidden className=\"size-3.5 shrink-0 text-ink-3\" />\n                <span className=\"sr-only\">{i18n(\"searchedFor\")} </span>\n                {query}\n              </li>\n            )}\n            {rows.map((row, i) => (\n              <TraceRow\n                key={row.id ?? `${i}-${row.primary}`}\n                classNames={classNames}\n                row={row}\n                variant={variant}\n                live={running && i === rows.length - 1}\n              />\n            ))}\n            {!!more && (\n              // arriving at the end of a run it grows in like a step; in history it is simply there\n              <li\n                data-slot=\"reasoning-more\"\n                className={cn(\"text-xs text-ink-3\", ran.current && \"enter-grow\", classNames?.more)}\n              >\n                <span>{i18n(\"more\", { more: more })}</span>\n              </li>\n            )}\n          </ol>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nfunction TraceRow({\n  row,\n  variant,\n  live,\n  classNames,\n}: {\n  row: ReasoningRow;\n  variant: ReasoningVariant;\n  live: boolean;\n  classNames?: ReasoningClassNames;\n}) {\n  const i18n = useMessages(\"reasoning\", englishMessages);\n\n  const reduced = useReducedMotionSafe();\n  // a step that arrives while the agent works grows to its height, so what sits below glides down instead of jumping\n  // a row; steps that mount with the trace (history) are simply there\n  const [arrived] = useState(live);\n  const site = host(row.href);\n  const body = (\n    <>\n      {variant === \"steps\" &&\n        (live ? (\n          <span aria-hidden className=\"grid size-3.5 shrink-0 place-items-center\">\n            <Spinner size=\"sm\" className=\"!size-3.5\" />\n          </span>\n        ) : (\n          <Check aria-hidden className=\"size-3.5 shrink-0 text-ink-3\" />\n        ))}\n      {variant === \"search\" && (\n        <span\n          aria-hidden\n          className=\"grid size-4 shrink-0 place-items-center rounded-chip bg-hover-2 text-2xs font-medium text-ink-2 uppercase\"\n        >\n          {(site || row.primary)[0]}\n        </span>\n      )}\n      <span\n        data-slot=\"reasoning-primary\"\n        className={cn(\n          \"min-w-0 text-xs\",\n          variant === \"reasoning\" ? \"leading-relaxed text-ink-2\" : \"truncate text-ink\",\n          variant === \"search\" && \"group-hover/row:underline\",\n          classNames?.primary,\n        )}\n      >\n        {row.primary}\n      </span>\n      {row.secondary && (\n        // the secondary gives way first when the row is short of room (a source's host folds away under 20rem)\n        <span\n          data-slot=\"reasoning-secondary\"\n          className={cn(\n            \"min-w-[3ch] shrink-[3] truncate text-2xs text-ink-3\",\n            row.mono && \"font-mono\",\n            variant === \"search\" && \"@max-xs:hidden\",\n            classNames?.secondary,\n          )}\n        >\n          {row.secondary}\n        </span>\n      )}\n      {row.add !== undefined && (\n        <span\n          data-slot=\"reasoning-delta\"\n          className={cn(\"shrink-0 font-numeric text-2xs tabular-nums\", classNames?.delta)}\n        >\n          <span className=\"text-up\">+{row.add}</span> <span className=\"text-down\">−{row.del ?? 0}</span>\n          <span className=\"sr-only\"> {i18n(\"lines\")}</span>\n        </span>\n      )}\n      {variant === \"steps\" && <span className=\"sr-only\">{live ? i18n(\"inProgress\") : i18n(\"done\")}</span>}\n    </>\n  );\n  const cls = cn(\n    \"group/row flex min-h-7 w-full items-center gap-2 rounded-chip py-0.5 text-start\",\n    arrived && !reduced && \"motion-safe:animate-enter-rise\",\n  );\n  const full = [row.primary, row.secondary].filter(Boolean).join(\" · \");\n  return (\n    <li data-slot=\"reasoning-row\" className={cn(arrived && !reduced && \"enter-grow\", classNames?.row)}>\n      <div data-slot=\"reasoning-rowClip\" className=\"min-w-0\">\n        {variant === \"search\" && row.href ? (\n          <a\n            data-slot=\"reasoning-rowContent\"\n            href={row.href}\n            target=\"_blank\"\n            rel=\"noreferrer\"\n            title={full}\n            className={cn(cls, \"-ms-1.5 px-1.5 hover:bg-hover\", classNames?.rowContent)}\n          >\n            {body}\n          </a>\n        ) : (\n          <div\n            data-slot=\"reasoning-rowContent\"\n            title={variant === \"reasoning\" ? undefined : full}\n            className={cn(cls, classNames?.rowContent)}\n          >\n            {body}\n          </div>\n        )}\n      </div>\n    </li>\n  );\n}\n\nexport default Reasoning;\n"},{"path":"beamline/components/slot-text/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/slot-text/defaults.ts","content":"/** Pure allowed presentation defaults for SlotText; no component or runtime imports. */\nexport interface SlotTextDefaults {\n  /** Shared align setting; omit to inherit the surrounding scope or the component default. */\n  align?: \"start\" | \"end\";\n}\n"},{"path":"beamline/components/slot-text/slot-text.module.css","type":"registry:file","target":"src/ui/beamline/components/slot-text/slot-text.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* A reel stands on the run of digits it is rolling to; once it lands it is quietly reset to the same digit on its\n     middle run (no motion), so the next roll can turn either way without running out of reel. A roll is an animation\n     from the numeral it showed to the one it lands on, in whole numerals (the staircase timing in slot-text.tsx), so it\n     always starts from a whole digit, even when a new value arrives mid-roll; the two names take turns so each change\n     starts afresh. */\n  .reel {\n    position: absolute;\n    inset-inline: 0;\n    top: 0;\n    display: flex;\n    flex-direction: column;\n    animation-fill-mode: both;\n  }\n  .rollA {\n    animation-name: slot-text-roll-a;\n  }\n  .rollB {\n    animation-name: slot-text-roll-b;\n  }\n  /* the inline transform names the target; a landed reel is reset over it */\n  .reel[data-settled] {\n    transform: translateY(calc(var(--_rest) * -1lh)) !important;\n    animation: none !important;\n  }\n\n  /* a new place or a changed sign waits, unseen, for its turn in the ripple (its delay is the animation's length), then\n     is there whole: never half faded over its neighbour */\n  .arrive {\n    animation-name: slot-text-wait;\n    animation-timing-function: step-end;\n  }\n  @keyframes slot-text-roll-a {\n    from {\n      transform: translateY(calc(var(--_from) * -1lh));\n    }\n    to {\n      transform: translateY(calc(var(--_to) * -1lh));\n    }\n  }\n  @keyframes slot-text-roll-b {\n    from {\n      transform: translateY(calc(var(--_from) * -1lh));\n    }\n    to {\n      transform: translateY(calc(var(--_to) * -1lh));\n    }\n  }\n  @keyframes slot-text-wait {\n    from,\n    to {\n      visibility: hidden;\n    }\n  }\n}\n"},{"path":"beamline/components/slot-text/slot-text.tsx","type":"registry:file","target":"src/ui/beamline/components/slot-text/slot-text.tsx","content":"\"use client\";\n\nimport { type AnimationEvent, type Ref } from \"react\";\nimport { useComposedRef } from \"../../hooks/use-composed-ref\";\nimport { useMotionTokens } from \"../../hooks/use-motion-tokens\";\nimport { useReducedMotionSafe } from \"../../hooks/use-reduced-motion-safe\";\n\nimport { useLocale, numberFormatter } from \"../../runtime/localization\";\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { SlotTextDefaults } from \"./defaults\";\nexport type { SlotTextDefaults } from \"./defaults\";\n\nimport { useEffect, useLayoutEffect, useRef, useState, type CSSProperties } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./slot-text.module.css\";\n\nexport interface SlotTextClassNames {\n  /** Classes for the root part; no override by default. */\n  root?: string;\n  /** Classes for the spoken part; no override by default. */\n  spoken?: string;\n  /** Classes for the visual part; no override by default. */\n  visual?: string;\n  /** Classes for the digit part; no override by default. */\n  digit?: string;\n  /** Classes for the measure part; no override by default. */\n  measure?: string;\n  /** Classes for the reel part; no override by default. */\n  reel?: string;\n  /** Classes for the numeral part; no override by default. */\n  numeral?: string;\n  /** Classes for the character part; no override by default. */\n  character?: string;\n}\n\nexport interface SlotTextProps {\n  /** Text or number shown on the animated reels. Required. */\n  value: string | number;\n  /** Turns a number into its text: currency, thousands. */\n  format?: (value: number) => string;\n  /** ms per reel. Default: twice the considered duration (about 0.7 s), following the motion scale. */\n  duration?: number;\n  /** ms between reels, right to left. Default: the word stagger (about 30 ms), following the motion scale. */\n  stagger?: number;\n  /** Extra full turns a changing digit makes. Default 0. */\n  spins?: number;\n  /** Which side stays put when the length changes. Default end (numbers). */\n  align?: \"start\" | \"end\";\n  /** Read the new value aloud when it changes. Default false (most counters are glanceable). */\n  announce?: boolean;\n  /** Content direction. Numbers stay LTR; other text resolves its own direction instead of inheriting page layout. */\n  dir?: \"ltr\" | \"rtl\" | \"auto\";\n  /** Classes on the documented visual root; no override by default. */\n  className?: string;\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: SlotTextClassNames;\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}\n\nconst DIGITS = \"0123456789\";\n\n/** A roll of `n` numerals as a staircase on the system's ease-out: each numeral is shown whole and held until the eased\n *  travel reaches the next, so the reel slows into place like an odometer and no frame ever shows two digits split by\n *  the window (the owner rejected digits drawn over each other mid-roll, 27 Oct). */\nconst stairs = new Map<number, string>();\nfunction staircase(n: number) {\n  let s = stairs.get(n);\n  if (!s) {\n    // ease-out cubic: travel p at time t = 1 - (1 - p)^(1/3); numeral k shows once the travel passes the middle of k\n    const at = (k: number) => `${((1 - Math.cbrt(1 - (k - 0.5) / n)) * 100).toFixed(2)}%`;\n    const points = [\"0 0%\"];\n    for (let k = 1; k <= n; k++) points.push(`${(k - 1) / n} ${at(k)}`, `${k / n} ${at(k)}`);\n    points.push(\"1 100%\");\n    s = `linear(${points.join(\", \")})`;\n    stairs.set(n, s);\n  }\n  return s;\n}\n\n/** A caller's ms, or the token default, on the system's clock. */\nconst clock = (ms: number | undefined, token: string) =>\n  ms === undefined ? token : `calc(${ms}ms * var(--pui-motion-scale, 1) * var(--pui-motion-look, 1))`;\n\n/**\n * Text and numbers that roll into their new value on reels, digit by digit from the right. A number that goes up rolls\n * every reel up and one that goes down rolls them down (9 → 0 on the way up passes through the wrap, like an odometer),\n * so the direction of the change reads before the digits do. A reel ticks through whole numerals, slowing into place, so\n * any frame taken mid-roll reads clean digits; a place the new value no longer has goes at once, and a new place or a\n * changed sign appears whole when its turn in the ripple comes. Reduced motion swaps the value at once. The reels are\n * hidden from assistive tech, which reads the value as text.\n */\nexport function SlotText({\n  value,\n  format,\n  duration,\n  stagger,\n  spins = 0,\n  align: alignProp,\n  announce = false,\n  dir,\n  className,\n  classNames,\n  ref,\n  style,\n}: SlotTextProps) {\n  const locale = useLocale();\n  const scopeDefaults = useComponentDefaults<SlotTextDefaults>(\"slot-text\", [\"align\"]);\n  const align = alignProp ?? scopeDefaults.align ?? \"end\";\n\n  const text =\n    typeof value === \"number\"\n      ? format\n        ? format(value)\n        : numberFormatter(locale, { maximumSignificantDigits: 21 }).format(value)\n      : value;\n  const direction = dir ?? (typeof value === \"number\" || !/\\p{L}/u.test(text) ? \"ltr\" : \"auto\");\n  const host = useRef<HTMLSpanElement>(null);\n  const attach = useComposedRef(host, ref);\n  const tokens = useMotionTokens(host);\n  const managedReduced = useReducedMotionSafe();\n  const reduced = tokens.reduced || managedReduced;\n  // the change is known in the same render it arrives in (no effect-delayed second pass): what it was, which way a\n  // number went, and a run counter that restarts the character fades\n  const [seen, setSeen] = useState({ text, value });\n  const [from, setFrom] = useState(text);\n  const [trend, setTrend] = useState(0);\n  const [run, setRun] = useState(0);\n  if (text !== seen.text) {\n    setSeen({ text, value });\n    setFrom(seen.text);\n    setTrend(typeof value === \"number\" && typeof seen.value === \"number\" ? Math.sign(value - seen.value) : 0);\n    setRun((r) => r + 1);\n  }\n  // when the roll has landed, the columns the old value needed and the new one does not are let go\n  const settle = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n  useEffect(() => {\n    if (from === text) return;\n    const s = tokens.duration.considered * 2 + tokens.stagger.word * Math.max(text.length, from.length);\n    const ms = Math.max((duration ?? s * 1000) + (stagger ?? 0) * Math.max(text.length, from.length), 0) + 50;\n    settle.current = setTimeout(() => setFrom(text), reduced ? 0 : ms);\n    return () => clearTimeout(settle.current);\n  }, [from, text, run, duration, stagger, reduced, tokens.duration.considered, tokens.stagger.word]);\n  // a new roll starts from where each reel rests: the resting mark from the last roll is lifted before paint\n  useLayoutEffect(() => {\n    host.current?.querySelectorAll(\"[data-settled]\").forEach((r) => r.removeAttribute(\"data-settled\"));\n  }, [run]);\n  const len = Math.max(text.length, from.length);\n  const pad = (s: string) => (align === \"end\" ? s.padStart(len, \" \") : s.padEnd(len, \" \"));\n  const now = pad(text),\n    was = pad(from);\n  // a reel holds the digits 2·spins + 3 times; it rests on the middle run, and a roll that passes the wrap lands on\n  // the run beside it, from which the reel is quietly reset to the middle once it stops\n  const base = 10 * (spins + 1);\n  const reel = Array.from({ length: 10 * (2 * spins + 3) }, (_, k) => DIGITS[k % 10]);\n  const durationCss = clock(duration, \"calc(var(--pui-duration-considered) * 2)\");\n  const staggerCss = clock(stagger, \"var(--pui-stagger-word)\");\n  const landed = (e: AnimationEvent<HTMLSpanElement>) => {\n    const target = e.target as HTMLElement;\n    if (target.dataset.slot === \"slot-text-reel\") target.setAttribute(\"data-settled\", \"\");\n  };\n  return (\n    <span\n      ref={attach}\n      data-slot=\"slot-text\"\n      data-trend={trend > 0 ? \"up\" : trend < 0 ? \"down\" : undefined}\n      dir={direction}\n      className={cn(\"relative inline-flex tabular-nums [unicode-bidi:isolate]\", classNames?.root, className)}\n      style={style}\n    >\n      <span\n        data-slot=\"slot-text-spoken\"\n        className={cn(\"sr-only\", classNames?.spoken)}\n        aria-live={announce ? \"polite\" : undefined}\n      >\n        {text}\n      </span>\n      <span\n        aria-hidden\n        data-slot=\"slot-text-visual\"\n        className={cn(\"inline-flex overflow-hidden\", classNames?.visual)}\n        onAnimationEnd={landed}\n      >\n        {[...now].map((ch, i) => {\n          const order = align === \"end\" ? len - 1 - i : i;\n          // a column keeps its element while it stays the same distance from the aligned side, so it can roll\n          const key = align === \"end\" ? `e${order}` : `s${order}`;\n          const delay = `calc(${order} * ${staggerCss})`;\n          const old = was[i];\n          // a place the new value no longer has (1,284 → 488: the \"1\" and the \",\"): gone at once, so the number takes\n          // its new width with the change itself and no half-faded digit sits over its neighbour while the reels turn\n          const padding = align === \"end\" ? i < len - text.length : i >= text.length;\n          if (padding) return null;\n          if (DIGITS.includes(ch)) {\n            const d = Number(ch);\n            const before = Number(old);\n            const changed = old !== ch && DIGITS.includes(old) && run > 0;\n            const turns = changed ? spins * 10 : 0;\n            // the way the number went decides the way every reel turns; a text value turns each the short way\n            const to =\n              !changed || reduced\n                ? base + d\n                : trend > 0\n                  ? base + d + (d > before ? 0 : 10) + turns\n                  : trend < 0\n                    ? base + d - (d < before ? 0 : 10) - turns\n                    : base + d + turns;\n            const fresh = !DIGITS.includes(old) && run > 0 && !reduced;\n            const start = changed && !reduced ? base + before : to;\n            const steps = Math.abs(to - start);\n            return (\n              <span\n                key={key}\n                data-slot=\"slot-text-digit\"\n                className={cn(\n                  \"relative inline-block h-[1lh] overflow-hidden\",\n                  fresh && styles.arrive,\n                  classNames?.digit,\n                )}\n                style={fresh ? { animationDuration: delay } : undefined}\n              >\n                <span data-slot=\"slot-text-measure\" className={cn(\"invisible\", classNames?.measure)}>\n                  0\n                </span>\n                <span\n                  data-slot=\"slot-text-reel\"\n                  className={cn(styles.reel, steps > 0 && (run % 2 ? styles.rollA : styles.rollB), classNames?.reel)}\n                  style={\n                    {\n                      \"--_from\": start,\n                      \"--_to\": to,\n                      \"--_rest\": base + d,\n                      transform: \"translateY(calc(var(--_to) * -1lh))\",\n                      ...(run > 0 &&\n                        !reduced && {\n                          animationDuration: durationCss,\n                          animationDelay: delay,\n                          animationTimingFunction: staircase(Math.max(steps, 1)),\n                        }),\n                    } as CSSProperties\n                  }\n                >\n                  {reel.map((n, k) => (\n                    <span key={k} data-slot=\"slot-text-numeral\" className={cn(\"h-[1lh]\", classNames?.numeral)}>\n                      {n}\n                    </span>\n                  ))}\n                </span>\n              </span>\n            );\n          }\n          return (\n            <span\n              key={`${key}-${ch}-${run}`}\n              data-slot=\"slot-text-character\"\n              className={cn(\n                \"inline-block whitespace-pre\",\n                !reduced && old !== ch && run > 0 && styles.arrive,\n                classNames?.character,\n              )}\n              style={{ animationDuration: delay }}\n            >\n              {ch}\n            </span>\n          );\n        })}\n      </span>\n    </span>\n  );\n}\n\nexport default SlotText;\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/components/text-shimmer/messages.ts","type":"registry:file","target":"src/ui/beamline/components/text-shimmer/messages.ts","content":"import type { MessageDefinitions } from \"../../runtime/messages\";\n/** Built-in English defaults. */\nexport const english = {\n  durationParts: \"{minutes} {seconds}\",\n  took: \"took {duration}\",\n} satisfies MessageDefinitions;\n"},{"path":"beamline/components/text-shimmer/text-shimmer.module.css","type":"registry:file","target":"src/ui/beamline/components/text-shimmer/text-shimmer.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* the words carry the band of light (clipped to the glyphs) while work runs; when it ends their ink fades back in\n     over the band, so they settle instead of snapping */\n  .root {\n    display: inline-block;\n    background-image: linear-gradient(\n      90deg,\n      var(--pui-ink-3) 0%,\n      var(--pui-ink-3) 40%,\n      var(--pui-ink) 50%,\n      var(--pui-ink-3) 60%,\n      var(--pui-ink-3) 100%\n    );\n    background-size: 250% 100%;\n    background-clip: text;\n    color: var(--pui-ink-2);\n    transition: color var(--pui-motion-out) var(--pui-ease-default);\n  }\n  .root[data-moving] {\n    color: transparent;\n    transition-duration: 0ms;\n  }\n  /* the time and the room it holds share one cell: the cell is as wide as the wider of the two, so the time showing,\n     or growing from 9s to 10s, moves nothing; at a minute and at ten the room widens, eased (it clips while it eases) */\n  .elapsed {\n    display: inline-grid;\n    overflow: clip;\n    color: var(--pui-ink-3);\n    font-variant-numeric: tabular-nums;\n  }\n  .elapsed > [aria-hidden] {\n    grid-area: 1 / 1;\n    white-space: pre;\n  }\n  .room {\n    visibility: hidden;\n  }\n  .room::before {\n    content: attr(data-room);\n  }\n}\n"},{"path":"beamline/components/text-shimmer/text-shimmer.tsx","type":"registry:file","target":"src/ui/beamline/components/text-shimmer/text-shimmer.tsx","content":"\"use client\";\n\nimport { useMessages } from \"../../runtime/localization\";\nimport type { Translator } from \"../../runtime/messages\";\nimport { english } from \"./messages\";\nimport { useEffect, useRef, useState, 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 { useWidthMorph } from \"../../hooks/use-width-morph\";\nimport { useReducedMotionSafe } from \"../../hooks/use-reduced-motion-safe\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./text-shimmer.module.css\";\n\nexport interface TextShimmerClassNames {\n  root?: string;\n  elapsed?: string;\n}\n\nexport interface TextShimmerProps {\n  children: string;\n  /** While work runs a calm band of light crosses the words; false shows plain text. Default true. */\n  running?: boolean;\n  /** Seconds per pass. Default 2. */\n  duration?: number;\n  /** After two seconds of work, how long it has taken (\"· 12s\"), counted while it runs and kept when it stops. */\n  elapsed?: boolean;\n  as?: \"span\" | \"div\" | \"p\" | \"h2\" | \"h3\" | \"h4\";\n  className?: string;\n  id?: string;\n  classNames?: TextShimmerClassNames;\n  style?: CSSProperties;\n  ref?: Ref<HTMLElement>;\n}\n\nfunction took(s: number, i18n: Translator<typeof english>, spoken = false) {\n  const part = (value: number, unit: \"second\" | \"minute\", pad = false) =>\n    i18n.number(value, {\n      style: \"unit\",\n      unit,\n      unitDisplay: spoken ? \"long\" : \"narrow\",\n      useGrouping: false,\n      minimumIntegerDigits: pad && !spoken ? 2 : 1,\n    });\n  return s < 60\n    ? part(s, \"second\")\n    : i18n(\"durationParts\", { minutes: part(Math.floor(s / 60), \"minute\"), seconds: part(s % 60, \"second\", true) });\n}\n\n/** Ongoing work said in words (\"Thinking…\", \"Syncing\"), with a band of light passing across them, in the reading\n *  direction. It costs a background-position animation and stops when it is not running or off screen; when the work\n *  ends the words settle into plain ink instead of snapping, and a screen reader hears the end once. With `elapsed`,\n *  work that takes longer than two seconds says how long. Reduced motion keeps the text still. */\nexport function TextShimmer({\n  children,\n  running = true,\n  duration = 2,\n  elapsed = false,\n  as: Tag = \"span\",\n  className,\n  id,\n  classNames,\n  style,\n  ref,\n}: TextShimmerProps) {\n  const i18n = useMessages(\"text-shimmer\", english);\n  const host = useRef<HTMLElement>(null);\n  const attach = useComposedRef(host, ref);\n  const active = useActive(host);\n  const tokens = useMotionTokens(host);\n  const managedReduced = useReducedMotionSafe();\n  const moving = running && !(tokens.reduced || managedReduced);\n  // seconds of work: counted once a second while it runs (a timer that ends with the work), kept when it stops\n  const [seconds, setSeconds] = useState(0);\n  const [wasRunning, setWasRunning] = useState(running);\n  // a line that has run stays a status: the region is there before its words change, so the end is read once\n  const [ran, setRan] = useState(running);\n  if (running !== wasRunning) {\n    setWasRunning(running);\n    if (running) setSeconds(0);\n    if (running && !ran) setRan(true);\n  }\n  // the room changes shape only at a minute and at ten (· 00s → · 0m 00s → · 00m 00s): there it eases, so the\n  // line widens in a glide rather than in one frame; reduced motion and an off-screen line take it at once\n  const room = took(seconds < 60 ? 10 : seconds, i18n).replace(/\\p{Number}/gu, i18n.number(0));\n  const clock = useRef<HTMLSpanElement>(null);\n  useWidthMorph(clock, room);\n  useEffect(() => {\n    if (!elapsed || !running) return;\n    const start = Date.now();\n    // organism-allow: elapsed-copy -- the count holds its last value once the work stops (\"took 12s\")\n    const tick = window.setInterval(() => setSeconds(Math.floor((Date.now() - start) / 1000)), 1000);\n    return () => window.clearInterval(tick);\n  }, [elapsed, running]);\n  return (\n    <Tag\n      ref={attach}\n      id={id}\n      data-slot=\"text-shimmer\"\n      data-running={running ? \"\" : undefined}\n      data-moving={moving ? \"\" : undefined}\n      role={running || ran ? \"status\" : undefined}\n      className={cn(\n        styles.root,\n        moving && \"motion-safe:animate-[pui-shimmer_var(--_shimmer)_linear_infinite] rtl:[animation-direction:reverse]\",\n        classNames?.root,\n        className,\n      )}\n      // the pass length follows the global speed (O1 §14.1)\n      style={\n        {\n          \"--_shimmer\": `calc(${duration}s * var(--pui-motion-scale, 1) * var(--pui-motion-look, 1))`,\n          animationPlayState: active ? \"running\" : \"paused\",\n          ...style,\n        } as CSSProperties\n      }\n    >\n      {children}\n      {elapsed && (running || seconds >= 2) && (\n        // its own ink, so the band never crosses it; silent while it counts (never announced every second), read\n        // once in words when the work ends. The room the time needs (\"· 00s\") is held from the start of the work, so\n        // nothing beside the line moves when the time shows at two seconds or goes from 9s to 10s\n        <span ref={clock} data-slot=\"text-shimmer-elapsed\" className={cn(styles.elapsed, classNames?.elapsed)}>\n          {/* drawn by CSS from data-room, so the held room adds no words to the line's text */}\n          <span aria-hidden className={styles.room} data-room={` · ${room}`} />\n          {seconds >= 2 && (\n            <span aria-hidden className=\"animate-enter-fade\">\n              {\" \"}\n              · {took(seconds, i18n)}\n            </span>\n          )}\n          {!running && seconds >= 2 && (\n            <span className=\"sr-only\">, {i18n(\"took\", { duration: took(seconds, i18n, true) })}</span>\n          )}\n        </span>\n      )}\n    </Tag>\n  );\n}\n\nexport default TextShimmer;\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-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-deprecated-prop.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-deprecated-prop.ts","content":"\"use client\";\n\nimport { useEffect } from \"react\";\n\n// Bundlers replace NODE_ENV for browser builds; this declaration emits no Node dependency.\ndeclare const process: { env: { NODE_ENV?: string } };\n\nconst warned = new Set<string>();\n\n/** Report a legacy prop once after mount in development; production and server renders stay silent. */\nexport function useDeprecatedProp(component: string, prop: string, replacement: string, supplied: boolean) {\n  useEffect(() => {\n    if (process.env.NODE_ENV !== \"development\" || !supplied) return;\n    const key = `${component}.${prop}`;\n    if (warned.has(key)) return;\n    warned.add(key);\n    console.warn(`[Beamline] ${key} is deprecated. Use ${replacement} instead; the legacy prop still works.`);\n  }, [component, prop, replacement, supplied]);\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/hooks/use-width-morph.ts","type":"registry:file","target":"src/ui/beamline/hooks/use-width-morph.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\n/**\n * A label whose text changes keeps its path: when `key` changes, the element's width eases from what it was to what the\n * new text needs, so whatever sits after it (a chevron, a counter) glides instead of jumping. The element clips while it\n * eases (give it `overflow-clip whitespace-nowrap`). Nothing runs at rest; reduced motion and motion \"none\" switch at\n * once, and so does a label off screen.\n */\nexport function useWidthMorph(ref: RefObject<HTMLElement | null>, key: unknown) {\n  const motion = useMotionTokens(ref);\n  const last = useRef<number | null>(null);\n  const easing = useRef<Animation | null>(null);\n  // the width it last rested at, kept current when it changes on its own (a web font arriving) so the next ease starts\n  // where the label really is; the ease's own frames are not a resting width\n  useLayoutEffect(() => {\n    const el = ref.current;\n    if (!el || typeof ResizeObserver === \"undefined\") return;\n    const observer = new ResizeObserver(() => {\n      if (!easing.current) last.current = el.getBoundingClientRect().width;\n    });\n    observer.observe(el);\n    return () => observer.disconnect();\n  }, [ref]);\n  useLayoutEffect(() => {\n    const el = ref.current;\n    if (!el) return;\n    // mid-way through an earlier change, it starts from where it is now, not from where that change was going\n    const from = easing.current ? el.getBoundingClientRect().width : last.current;\n    easing.current?.cancel();\n    easing.current = null;\n    const box = el.getBoundingClientRect();\n    const to = box.width;\n    last.current = to;\n    const ms = motion.duration.morph * 1000;\n    if (from === null || Math.abs(from - to) < 0.5 || motion.reduced || ms < 2) return;\n    // off screen nobody sees the glide: the label takes its width at once and no frame is spent on it\n    if (box.bottom < 0 || box.top > innerHeight || box.right < 0 || box.left > innerWidth) return;\n    const ease = motion.ease.default;\n    // pixel to pixel: a Web Animations keyframe to \"auto\" (with interpolate-size, or calc-size) jumps straight to the\n    // end in Chromium 153 instead of easing (measured: 537 of 537 px at 300 of 1000 ms); a late change of the natural\n    // width is caught by the observer above when the ease ends\n    const animation = el.animate([{ width: `${from}px` }, { width: `${to}px` }], {\n      duration: ms,\n      // even in and out: the glide never moves a neighbour more than a pixel or two in one frame\n      easing: cssEase(ease, \"ease-in-out\"),\n    });\n    easing.current = animation;\n    void animation.finished.then(\n      () => {\n        if (easing.current !== animation) return;\n        easing.current = null;\n        last.current = el.getBoundingClientRect().width;\n      },\n      () => {},\n    );\n  }, [key]); // eslint-disable-line react-hooks/exhaustive-deps -- eases on a change of the text it stands for\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":"Reasoning is in src/ui/beamline/components/reasoning/reasoning.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/reasoning"}}