{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "thinking",
  "type": "registry:ui",
  "title": "Thinking",
  "description": "The agent's reasoning as a quiet disclosure above its answer: a row with a chevron, \"Thinking\" and a clock that counts whole seconds, never a shimmer, a pulse or a spinner, then \"Thought for 12s\". The reasoning sits under it in the quiet text tone with no box. It is open while the agent thinks and closes itself when it stops, unless the reader toggled it.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://littmd.vercel.app/r/litt.json",
    "https://littmd.vercel.app/r/icons.json",
    "https://littmd.vercel.app/r/motion.json"
  ],
  "files": [
    {
      "path": "registry/litt/ui/thinking.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n// @litt/thinking 2026.11.08. What changed: https://littmd.vercel.app/changelog.md\n\n/**\n * Thinking: the agent's reasoning as a quiet disclosure above its answer. While the agent thinks it reads\n * \"Thinking 4s\" with the reasoning streaming in underneath; when it stops it reads \"Thought for 12s\" and folds\n * itself away. The reader can open any thought and read it.\n *\n * - Row (components.md, Thinking): a button 32px tall (44px on a coarse pointer, or inside a phone\n *   DeviceProvider) with no fill and no padding, so the chevron sits on the column's edge. A 16px chevron, the label\n *   at the small size in fg-muted and the clock in the caption in fg-subtle with tabular numbers. Hover steps\n *   the label to fg, and a press on a finger does the same. The text sits on a shared baseline, so the clock never\n *   rides high or low beside the label.\n * - Flat: a grey card at the surface radius with no frame; the reasoning keeps its indent inside it.\n * - No shimmer, no pulse, no spinner, no dots, no caret (R2, and \"no motion that is not a response to the person\").\n *   The only live signal is the clock, which counts whole seconds from when `active` turned true. It is a number\n *   changing, not an animation.\n * - Clock words: under a minute \"12s\", then \"1m 5s\", a whole minute \"2m\", and past an hour \"1h 5m\". Seconds\n *   are always rounded down. A thought that lasted\n *   under a second reads \"Thought for a moment\", with no clock. A thought with no known time (never active, no\n *   `duration`) reads just \"Thought\". `label` replaces the words and keeps the clock: \"Reasoning 4s\".\n * - Body: the reasoning at the small size in fg-subtle, indented by the chevron and its gap so it lines up with the\n *   label, with no box, no rail and no tint. A grid with --litt-space-2 between paragraphs, so a fragment of <p>\n *   or a bare string both work. It holds to about 68ch, like an answer, and wraps long words instead of widening the page.\n * - Open and closed: uncontrolled, it is open while `active` and closes itself when `active` goes false, unless\n *   the reader toggled it during that thought, so a reader's choice always wins. A new thought opens it again.\n *   `open` and `onOpenChange` control it, and then the auto open and close are the caller's: only a reader's\n *   toggle calls `onOpenChange`. `defaultOpen` sets the first state, which is otherwise `active`.\n * - Motion: the chevron turns a quarter on the instant tier (the `rotate` property, nothing else), and the label's\n *   color fades on the instant tier. The body fades in and travels a quarter of the travel token down on the responsive\n *   tier, and leaves with a fade on the exit token. Its height snaps, never animates: the body unmounts when the fade\n *   ends. It has its own AnimatePresence, without `initial={false}`, so an ancestor's cannot swallow its entrance.\n * - Indicator: an optional slot beside the row while `active`, for an AgentIndicator under Device (decision 7 of\n *   worlds.md). It is a sibling of the button, not inside it, so the row's name is unchanged; it is 31px tall at\n *   the caption size, inside the row's 32px, so the row never changes height when the thought ends and it goes.\n *   Without the prop the DOM is exactly as it was.\n * - Semantics: a real button with aria-expanded and aria-controls, pointing at a region that is always in the DOM\n *   (empty while closed). The ticking clock is aria-hidden; the finished clock is read as words (\"12 seconds\").\n *   A polite live region says the label once when the thought starts and \"Thought for 12 seconds\" when it ends,\n *   in words. It stays silent for a thought loaded from history, and the reasoning is never announced as it streams.\n */\nimport { useEffect, useId, useLayoutEffect, useRef, useState, type ReactNode } from \"react\";\nimport { AnimatePresence, motion } from \"motion/react\";\nimport { ChevronRightIcon } from \"./icons\";\nimport { useDevice, useExit, useTransition, useTravel } from \"../../lib/litt/motion\";\n\n/* ----------------------------------------------------------------- the clock's words */\n\nconst whole = (n: number) => Math.max(0, Math.floor(n));\n\n/** \"12s\", \"1m 5s\", \"2m\", \"1h 5m\": what the row shows. Whole seconds, rounded down. */\nfunction formatClock(total: number): string {\n  const s = whole(total);\n  const h = Math.floor(s / 3600);\n  const m = Math.floor((s % 3600) / 60);\n  const r = s % 60;\n  if (h) return m ? `${h}h ${m}m` : `${h}h`;\n  if (m) return r ? `${m}m ${r}s` : `${m}m`;\n  return `${r}s`;\n}\n\n/** \"12 seconds\", \"1 minute 5 seconds\": what a screen reader should say where the row shows \"12s\". */\nfunction speakClock(total: number): string {\n  const s = whole(total);\n  const h = Math.floor(s / 3600);\n  const m = Math.floor((s % 3600) / 60);\n  const r = s % 60;\n  const unit = (n: number, one: string) => `${n} ${n === 1 ? one : `${one}s`}`;\n  if (h) return m ? `${unit(h, \"hour\")} ${unit(m, \"minute\")}` : unit(h, \"hour\");\n  if (m) return r ? `${unit(m, \"minute\")} ${unit(r, \"second\")}` : unit(m, \"minute\");\n  return unit(r, \"second\");\n}\n\n/** The words before the clock, for a finished thought. `seconds` is null when the time is not known. */\nfunction finishedWords(seconds: number | null): string {\n  if (seconds === null) return \"Thought\";\n  return seconds < 1 ? \"Thought for a moment\" : \"Thought for\";\n}\n\n/* ----------------------------------------------------------------- Thinking */\n\nexport type ThinkingProps = {\n  /** True while the agent is thinking. The clock counts whole seconds from when it turned true, the body is open, and when it turns false the thought is finished. */\n  active?: boolean;\n  /** Seconds a finished thought took, for one loaded from history. It is rounded down, and it is ignored while `active`. Without it, a thought that ran here shows the time its clock reached. */\n  duration?: number;\n  /** Whether the reasoning is open, when you control it. Pair it with onOpenChange. The automatic open and close are then yours. */\n  open?: boolean;\n  /** Whether it starts open when you do not control it. It starts open while `active` and closed otherwise. */\n  defaultOpen?: boolean;\n  /** Called with the new state when the reader toggles the row. */\n  onOpenChange?: (open: boolean) => void;\n  /** Replaces the words \"Thinking\" and \"Thought for\", such as \"Reasoning\". The clock still follows it: \"Reasoning 4s\", then \"Reasoning 12s\". */\n  label?: string;\n  /** The reasoning: paragraphs in the small size, spaced apart. It can be empty while the first words are still on their way. */\n  children?: ReactNode;\n  /** Something that shows what the agent is doing while it thinks, such as an AgentIndicator under Device. It sits beside the row while `active` and is gone once the thought is finished. Leave it out and the row is exactly as it was. */\n  indicator?: ReactNode;\n  /** Extra classes for the whole component, such as a width. */\n  className?: string;\n};\n\n/** The agent's reasoning as a disclosure: a row with a label and a clock, and the reasoning folded under it. Open while the agent thinks, closed once it has, and always the reader's to toggle. */\nexport function Thinking({\n  active = false,\n  duration,\n  open: openProp,\n  defaultOpen,\n  onOpenChange,\n  label,\n  children,\n  indicator,\n  className = \"\",\n}: ThinkingProps) {\n  const uid = useId();\n  const bodyId = `${uid}-body`;\n  const { coarse } = useDevice();\n  const enter = useTransition(\"responsive\");\n  const exit = useExit(\"responsive\");\n  const travel = useTravel();\n\n  const controlled = openProp !== undefined;\n  const [innerOpen, setInnerOpen] = useState(defaultOpen ?? active);\n  const open = controlled ? openProp : innerOpen;\n\n  /* The clock: whole seconds since `active` turned true. It keeps its last value once the thought ends, which is\n     what the finished label shows. */\n  const [elapsed, setElapsed] = useState(0);\n  const [ran, setRan] = useState(active);\n  const [touched, setTouched] = useState(false);\n  const [prevActive, setPrevActive] = useState(active);\n  const [said, setSaid] = useState(\"\");\n\n  /* A thought starting or stopping is adjusted while rendering, so the first frame after the change is already right:\n     a new thought starts at 0s and open, and a finished one closes itself unless the reader toggled it. */\n  if (active !== prevActive) {\n    setPrevActive(active);\n    if (active) {\n      setElapsed(0);\n      setRan(true);\n      setTouched(false);\n      setInnerOpen(true);\n    } else if (!touched) {\n      setInnerOpen(false);\n    }\n  }\n\n  /* Count from the moment it turned on. Each tick lands on the next whole second, so the clock does not drift, and\n     the time is read from the clock rather than counted, so a throttled background tab still shows the truth. A\n     layout effect, so the final reading is in place before the frame in which the thought reads as finished. */\n  const finished = useRef<number | null>(null);\n  useLayoutEffect(() => {\n    if (!active) return;\n    const start = performance.now();\n    const read = () => Math.floor((performance.now() - start) / 1000);\n    let timer: ReturnType<typeof setTimeout>;\n    const tick = () => {\n      setElapsed(read());\n      timer = setTimeout(tick, 1000 - ((performance.now() - start) % 1000));\n    };\n    timer = setTimeout(tick, 1000);\n    return () => {\n      clearTimeout(timer);\n      finished.current = read();\n      setElapsed(finished.current);\n    };\n  }, [active]);\n\n  const given = typeof duration === \"number\" && Number.isFinite(duration) ? whole(duration) : null;\n  const seconds = active ? elapsed : (given ?? (ran ? elapsed : null));\n  const words = label ?? (active ? \"Thinking\" : finishedWords(seconds));\n  const showClock = seconds !== null && (active || seconds >= 1);\n\n  /* The two announcements, in words: once at the start and once at the end. Nothing for a thought that was\n     loaded finished, and nothing while the reasoning streams. */\n  const announced = useRef(false);\n  useEffect(() => {\n    if (active) {\n      announced.current = true;\n      setSaid(label ?? \"Thinking\");\n    } else if (announced.current) {\n      announced.current = false;\n      const took = given ?? finished.current ?? seconds;\n      const spoken = took !== null && took >= 1 ? speakClock(took) : null;\n      setSaid(label ? (spoken ? `${label} finished after ${spoken}` : `${label} finished`) : spoken ? `Thought for ${spoken}` : finishedWords(took));\n    }\n    // Only a change of `active` is an announcement, and it says the words of the render that changed it.\n  }, [active]); // eslint-disable-line react-hooks/exhaustive-deps\n\n  function toggle() {\n    const next = !open;\n    setTouched(true);\n    if (!controlled) setInnerOpen(next);\n    onOpenChange?.(next);\n  }\n\n  const row = (\n    <button\n      type=\"button\"\n      aria-expanded={open}\n      aria-controls={bodyId}\n      onClick={toggle}\n      className={\n        \"group inline-flex max-w-full cursor-pointer items-center gap-2 rounded-litt-control text-litt-small font-normal text-litt-fg-muted select-none \" +\n        \"transition-[color] duration-(--litt-dur-instant) ease-litt [-webkit-tap-highlight-color:transparent] \" +\n        \"hover:text-litt-fg pointer-coarse:active:text-litt-fg pointer-coarse:h-litt-control-md \" +\n        \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-litt-focus \" +\n        (coarse ? \"h-litt-control-md\" : \"h-litt-control-sm\")\n      }\n    >\n      <ChevronRightIcon\n        className={\n          \"shrink-0 text-litt-fg-subtle transition-[rotate,color] duration-(--litt-dur-instant) ease-litt group-hover:text-litt-fg-muted \" +\n          (open ? \"rotate-90\" : \"rotate-0\")\n        }\n      />\n      <span className=\"flex min-w-0 items-baseline gap-2\">\n        <span className=\"min-w-0 truncate\">{words}</span>\n        {showClock && (\n          <span aria-hidden=\"true\" className=\"shrink-0 text-litt-caption font-normal tabular-nums text-litt-fg-subtle\">\n            {formatClock(seconds)}\n          </span>\n        )}\n      </span>\n      {/* A finished clock is read in words. A ticking one is never read. */}\n      {!active && showClock && <span className=\"sr-only\"> {speakClock(seconds)}</span>}\n    </button>\n  );\n\n  return (\n    <div\n      data-state={open ? \"open\" : \"closed\"}\n      className={\n        \"relative in-[[data-material=flat]]:max-w-(--litt-measure) in-[[data-material=flat]]:rounded-litt-surface in-[[data-material=flat]]:bg-litt-surface-3 in-[[data-material=flat]]:px-4 in-[[data-material=flat]]:py-1 \" +\n        \"in-[[data-material=flat]]:forced-colors:border \" +\n        className\n      }\n    >\n      {indicator === undefined ? (\n        row\n      ) : (\n        <div className=\"flex min-w-0 items-center gap-3\">\n          {row}\n          {active && <span className=\"shrink-0 text-litt-caption\">{indicator}</span>}\n        </div>\n      )}\n\n      {/* The region is always here, so aria-controls always points at something. Its own AnimatePresence keeps an\n          ancestor's initial={false} from swallowing the entrance. */}\n      <div id={bodyId}>\n        <AnimatePresence>\n          {open && (\n            <motion.div\n              key=\"body\"\n              initial={{ opacity: 0, y: -travel / 4 }}\n              animate={{ opacity: 1, y: 0, transition: enter }}\n              exit={{ opacity: 0, transition: exit }}\n              className=\"grid max-w-(--litt-measure) gap-2 pt-1 pb-1 pl-6 text-litt-small break-words text-litt-fg-subtle\"\n            >\n              {children}\n            </motion.div>\n          )}\n        </AnimatePresence>\n      </div>\n\n      <div role=\"status\" aria-live=\"polite\" aria-atomic=\"true\" className=\"sr-only\">\n        {said}\n      </div>\n    </div>\n  );\n}\n",
      "target": "components/litt/thinking.tsx"
    }
  ],
  "docs": "Import Thinking from components/litt/thinking. Pass active while the agent thinks and the reasoning as children: the clock counts from when active turned true. For a thought loaded from history leave active off and pass duration in seconds. open, defaultOpen and onOpenChange control it, and label replaces the words.",
  "meta": {
    "version": "2026.11.08",
    "released": "2026-11-08",
    "changelog": "https://littmd.vercel.app/changelog.md"
  }
}
