{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "thinking-bar",
  "title": "Thinking Bar",
  "description": "The line above a reasoning model's answer: the label shimmers while a seconds counter rolls, then \"Thinking\" morphs into \"Thought for 12s\" and the reasoning folds away a second after the answer starts.",
  "dependencies": [
    "motion",
    "@base-ui/react"
  ],
  "registryDependencies": [
    "https://eased.dev/r/eased-tokens.json",
    "https://eased.dev/r/eased-motion.json",
    "https://eased.dev/r/eased-auto-height.json",
    "https://eased.dev/r/eased-number-roll.json",
    "https://eased.dev/r/eased-text-swap.json",
    "https://eased.dev/r/eased-thinking-core.json",
    "https://eased.dev/r/shimmer-text.json"
  ],
  "files": [
    {
      "path": "src/registry/eased/thinking-bar/thinking-bar.tsx",
      "content": "\"use client\";\n\nimport { Collapsible } from \"@base-ui/react/collapsible\";\nimport { useEffect, useRef, useState, type HTMLProps, type ReactNode } from \"react\";\nimport { AutoHeight } from \"../_primitives/auto-height/auto-height\";\nimport { NumberRoll } from \"../_primitives/number-roll/number-roll\";\nimport { TextSwap } from \"../_primitives/text-swap/text-swap\";\nimport { formatElapsed, useElapsed, useStatusCarousel } from \"../_primitives/thinking-core/thinking-core\";\nimport { ShimmerText } from \"../shimmer-text/shimmer-text\";\n\nexport type ThinkingStatus = \"thinking\" | \"done\";\n\nexport type ThinkingBarProps = {\n  /** `thinking` shimmers and counts; `done` settles to \"Thought for 12s\". */\n  status: ThinkingStatus;\n  /** When thinking began (epoch ms or Date). Omit to count from when `status` turns `thinking`. */\n  startedAt?: number | Date;\n  /** When it ended. With `startedAt`, a reloaded message shows its real duration. */\n  endedAt?: number | Date;\n  /** The label while thinking. Default \"Thinking\". */\n  label?: string;\n  /**\n   * Status lines to rotate through after `label` while thinking, e.g. [\"Reading 4 sources\",\n   * \"Comparing plans\"]. Each holds 2–4s by length and swaps in; the last one holds.\n   */\n  lines?: string[];\n  /** The label once done, before the duration. Default \"Thought for\". */\n  doneLabel?: string;\n  /** Shown before the label: a Agent Orb, an icon. */\n  icon?: ReactNode;\n  /** The reasoning. When present the bar is a disclosure with a chevron; without it, a plain line. */\n  children?: ReactNode;\n  /** Open state (controlled). */\n  open?: boolean;\n  /** Initial open state (uncontrolled). Default: open while thinking when `autoOpen`. */\n  defaultOpen?: boolean;\n  /** Called for every change, the automatic ones included. */\n  onOpenChange?: (open: boolean) => void;\n  /** Open the reasoning while thinking. Default true. Stops once the user toggles it. */\n  autoOpen?: boolean;\n  /** Close the reasoning this many ms after `done`, or `false` to leave it. Default 1000. Stops once the user toggles it. */\n  autoClose?: number | false;\n  className?: string;\n};\n\nfunction Chevron() {\n  return (\n    <svg\n      viewBox=\"0 0 16 16\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={1.5}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      aria-hidden\n      className=\"size-4 shrink-0 [transition:rotate_var(--eased-duration-spring-smooth)_var(--eased-spring-smooth)] group-data-panel-open:rotate-90 reduced:[transition:none]\"\n    >\n      <path d=\"m6.5 4 4 4-4 4\" />\n    </svg>\n  );\n}\n\n/** Keeps a closing panel rendered until AutoHeight has collapsed it, then hides it (as Accordion does). */\nfunction PanelBody({ open, htmlProps }: { open: boolean; htmlProps: HTMLProps<HTMLDivElement> }) {\n  const [collapsed, setCollapsed] = useState(!open);\n  const { hidden, children, ...rest } = htmlProps;\n  void hidden;\n  return (\n    <div {...rest} hidden={!open && collapsed}>\n      <AutoHeight open={open} onOpenChangeComplete={(isOpen) => setCollapsed(!isOpen)} innerClassName=\"pt-2\">\n        <div className=\"border-l border-line-subtle pl-3 text-ui-sm text-text-muted text-pretty\">{children}</div>\n      </AutoHeight>\n    </div>\n  );\n}\n\n/**\n * The line above a reasoning model's answer. While thinking, the label shimmers and a seconds\n * counter rolls beside it; when done, \"Thinking\" swaps to \"Thought for\" and the counter, the\n * same element, glides along to read \"Thought for 12s\". With reasoning inside it is a\n * disclosure: it opens while thinking and folds away a second after the answer starts,\n * unless the reader has already opened or closed it themselves.\n */\nexport function ThinkingBar({\n  status,\n  startedAt,\n  endedAt,\n  label = \"Thinking\",\n  lines,\n  doneLabel = \"Thought for\",\n  icon,\n  children,\n  open: openProp,\n  defaultOpen,\n  onOpenChange,\n  autoOpen = true,\n  autoClose = 1000,\n  className = \"\",\n}: ThinkingBarProps) {\n  const thinking = status === \"thinking\";\n  const seconds = useElapsed({ running: thinking, startedAt, endedAt });\n  const rotation = [label, ...(lines ?? [])];\n  const line = useStatusCarousel(rotation, { paused: !thinking });\n  const hasBody = children !== undefined && children !== null && children !== false;\n\n  // Open state: controlled, or ours, driven by status until the reader takes over.\n  const [ownOpen, setOwnOpen] = useState(defaultOpen ?? (autoOpen && thinking));\n  const open = openProp ?? ownOpen;\n  const touched = useRef(false);\n  const latest = useRef({ onOpenChange, openProp });\n  useEffect(() => {\n    latest.current = { onOpenChange, openProp };\n  });\n  const setOpen = (next: boolean) => {\n    if (latest.current.openProp === undefined) setOwnOpen(next);\n    latest.current.onOpenChange?.(next);\n  };\n\n  useEffect(() => {\n    // A new thinking run hands control back to the automatic behaviour.\n    if (thinking) touched.current = false;\n    if (touched.current) return;\n    if (thinking) {\n      if (autoOpen) {\n        if (latest.current.openProp === undefined) setOwnOpen(true);\n        latest.current.onOpenChange?.(true);\n      }\n      return;\n    }\n    if (autoClose === false) return;\n    const t = window.setTimeout(() => {\n      if (touched.current) return;\n      if (latest.current.openProp === undefined) setOwnOpen(false);\n      latest.current.onOpenChange?.(false);\n    }, autoClose);\n    return () => clearTimeout(t);\n  }, [thinking, autoOpen, autoClose]);\n\n  const counted = seconds >= 1;\n  const text = thinking ? line : counted ? doneLabel : `${doneLabel} a moment`;\n  const spoken = thinking ? `${label}…` : counted ? `${doneLabel} ${seconds} ${seconds === 1 ? \"second\" : \"seconds\"}` : `${doneLabel} a moment`;\n\n  const title = (\n    <>\n      {icon && <span className=\"inline-flex shrink-0 items-center\">{icon}</span>}\n      <span aria-hidden className=\"inline-flex min-w-0 items-center\">\n        <ShimmerText active={thinking} className=\"text-current\">\n          {text}\n        </ShimmerText>\n        <TextSwap swapKey={counted ? \"on\" : \"off\"} className=\"tabular-nums\">\n          {counted ? (\n            <>\n              {\" \"}\n              <NumberRoll value={seconds} formatter={formatElapsed} aria-hidden />\n            </>\n          ) : null}\n        </TextSwap>\n      </span>\n      <span className=\"sr-only\">{spoken}</span>\n    </>\n  );\n\n  const row = \"inline-flex max-w-full items-center gap-1.5 text-ui text-text-muted\";\n  const live = (\n    <span role=\"status\" className=\"sr-only\">\n      {thinking ? \"\" : spoken}\n    </span>\n  );\n\n  if (!hasBody) {\n    return (\n      <div className={`min-w-0 ${className}`} aria-busy={thinking || undefined}>\n        <div className={row}>{title}</div>\n        {live}\n      </div>\n    );\n  }\n\n  return (\n    <Collapsible.Root\n      open={open}\n      onOpenChange={(next) => {\n        touched.current = true;\n        setOpen(next);\n      }}\n      className={`min-w-0 ${className}`}\n      aria-busy={thinking || undefined}\n    >\n      <Collapsible.Trigger\n        className={`group ${row} -mx-1 cursor-pointer rounded-control-xs px-1 text-left transition-colors duration-150 ease-standard focus-ring hover-fine:text-text-secondary`}\n      >\n        {title}\n        <Chevron />\n      </Collapsible.Trigger>\n      <Collapsible.Panel keepMounted render={(htmlProps, state) => <PanelBody open={state.open} htmlProps={htmlProps} />}>\n        {children}\n      </Collapsible.Panel>\n      {live}\n    </Collapsible.Root>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@components/eased/thinking-bar/thinking-bar.tsx"
    }
  ],
  "docs": "Import ThinkingBar from components/eased/thinking-bar/thinking-bar. Drive status from your reasoning stream; pass the reasoning as children.",
  "type": "registry:ui"
}