{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "thinking-state",
  "type": "registry:component",
  "title": "Thinking",
  "description": "Expandable traces — steps, reasoning, search, coding.",
  "categories": [
    "ai-interface"
  ],
  "registryDependencies": [
    "https://irisui.dev/r/foundation.json",
    "https://irisui.dev/r/utils.json"
  ],
  "files": [
    {
      "path": "src/registry/components/thinking-state/thinking-state.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useLayoutEffect, useRef, useState, type ReactNode } from \"react\";\nimport { cn } from \"../lib/utils\";\n\n/* ─────────────────────────────────────────────────────────\n * THINKING — expandable agent trace, four variants\n *\n *   Steps      step list with spinner → muted checks\n *   Reasoning  prose reasoning that expands, then settles\n *   Search     web-search trace: query + sources read\n *   Coding     tool trace: files read, edits, commands\n *\n * The trace runs once, settles, and remains expandable.\n * ───────────────────────────────────────────────────────── */\n\nconst STAGES = [800, 600, 1800, 2600, 1600];\n\nfunction useSequence(steps: number[]) {\n  const [stage, setStage] = useState(0);\n  useEffect(() => {\n    if (stage >= steps.length - 1) return;\n    const t = setTimeout(() => setStage((s) => s + 1), steps[stage]);\n    return () => clearTimeout(t);\n  }, [stage, steps]);\n  return stage;\n}\n\ntype Row = {\n  primary: string;\n  secondary?: string;\n  mono?: boolean;\n  add?: number;\n  del?: number;\n  href?: string;\n};\n\n/* a CapyDB agent preparing a migration */\nconst VARIANTS: Record<string, { active: string; done: string; rows: Row[]; query?: string }> = {\n  Steps: {\n    active: \"Thinking\",\n    done: \"Thought for 4 seconds\",\n    rows: [\n      { primary: \"Reading the shop schema\" },\n      { primary: \"Checking connection limits\" },\n      { primary: \"Comparing restore points\", secondary: \"6 backups\" },\n      { primary: \"Writing the migration plan\" },\n    ],\n  },\n  Reasoning: {\n    active: \"Thinking\",\n    done: \"Thought for 4 seconds\",\n    rows: [\n      { primary: \"shop uses 18 of 30 connections, so a preview database still leaves headroom.\" },\n      { primary: \"I should run migration 0042 on a preview before it touches production.\" },\n    ],\n  },\n  Search: {\n    active: \"Searching the docs\",\n    done: \"Searched the docs\",\n    query: \"point-in-time restore window\",\n    rows: [\n      { primary: \"Restore points and backups\", secondary: \"docs.capydb.example\", href: \"#\" },\n      { primary: \"Preview databases\", secondary: \"docs.capydb.example\", href: \"#\" },\n      { primary: \"Pause when idle\", secondary: \"changelog.capydb.example\", href: \"#\" },\n    ],\n  },\n  Coding: {\n    active: \"Running tools\",\n    done: \"Ran 3 tools\",\n    rows: [\n      { primary: \"Read\", secondary: \"schema.sql\", mono: true },\n      { primary: \"Edit\", secondary: \"0042_add_index.sql\", mono: true, add: 74, del: 41 },\n      { primary: \"Run\", secondary: \"npm run migrate\", mono: true },\n    ],\n  },\n};\n\nfunction Dot({ tone }: { tone: string }) {\n  return (\n    <span className={`flex size-3.5 shrink-0 items-center justify-center rounded-full text-white ${tone}`}>\n      <svg width=\"9\" height=\"9\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2.5\">\n        <circle cx=\"12\" cy=\"12\" r=\"9\" />\n        <path d=\"M3.5 12h17M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18\" />\n      </svg>\n    </span>\n  );\n}\n\nconst TONES = [\"bg-accent\", \"bg-orange\", \"bg-green\"];\n\nexport default function ThinkingState({\n  variant = \"Steps\",\n  onSettled,\n  rows,\n  active,\n  done,\n  icon,\n}: {\n  variant?: string;\n  onSettled?: () => void;\n  /** override the built-in trace content (keeps the primitive reusable) */\n  rows?: Row[];\n  active?: string;\n  done?: string;\n  /** override the header glyph (defaults to the sparkle) */\n  icon?: ReactNode;\n}) {\n  const stage = useSequence(STAGES);\n  const [manualExpanded, setManualExpanded] = useState<boolean | null>(null);\n  const [selectedTool, setSelectedTool] = useState<string | null>(null);\n  const base = VARIANTS[variant] ?? VARIANTS.Steps;\n  const v = {\n    ...base,\n    rows: rows ?? base.rows,\n    active: active ?? base.active,\n    done: done ?? base.done,\n  };\n  const autoExpanded = stage >= 1 && stage < 4;\n  const expanded = manualExpanded ?? autoExpanded;\n  const working = stage < 3;\n  const visible = stage < 2 ? 0 : stage === 2 ? Math.min(2, v.rows.length) : v.rows.length;\n  const traceRef = useRef<HTMLDivElement>(null);\n  const [lineHeight, setLineHeight] = useState(0);\n  useLayoutEffect(() => {\n    if (traceRef.current) setLineHeight(traceRef.current.offsetHeight);\n  }, [visible, expanded, variant, stage]);\n\n  /* let embedders sequence content after the trace settles */\n  const settledRef = useRef(false);\n  useEffect(() => {\n    if (working || settledRef.current) return;\n    settledRef.current = true;\n    onSettled?.();\n  }, [working, onSettled]);\n\n  return (\n    <div\n      key={variant}\n      className=\"flex w-full max-w-95 flex-col\"\n      style={{\n        minHeight: working || expanded ? 176 : undefined,\n        transition: \"min-height 400ms cubic-bezier(0.23,1,0.32,1)\",\n      }}\n    >\n      {/* header — shared across variants */}\n      <button\n        type=\"button\"\n        aria-expanded={expanded}\n        onClick={() => setManualExpanded((current) => !(current ?? autoExpanded))}\n        className=\"-mx-1.5 flex w-fit items-center gap-2 rounded-button px-1.5 py-1\n          transition-colors duration-100 hover:bg-hover-2\"\n      >\n        {icon ? (\n          <span\n            className=\"flex shrink-0 transition-colors duration-200\"\n            style={{ color: working ? \"var(--ink-2)\" : \"var(--ink-3)\" }}\n          >\n            {icon}\n          </span>\n        ) : (\n          <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill={working ? \"var(--ink-2)\" : \"var(--ink-3)\"}>\n            <path d=\"M12 2l2.4 7.2L22 12l-7.6 2.8L12 22l-2.4-7.2L2 12l7.6-2.8z\" />\n          </svg>\n        )}\n        <output className=\"contents\">\n          {working ? (\n            <span className=\"animate-thinking-state-shimmer bg-[linear-gradient(90deg,var(--ink-3)_35%,var(--ink)_50%,var(--ink-3)_65%)] bg-size-[200%_100%] bg-clip-text text-[13px] font-medium whitespace-nowrap text-transparent\">\n              {v.active}\n            </span>\n          ) : (\n            <span className=\"animate-thinking-state-done-in text-[13px] font-medium whitespace-nowrap text-ink-2\">\n              {v.done}\n            </span>\n          )}\n        </output>\n        <svg\n          width=\"14\"\n          height=\"14\"\n          viewBox=\"0 0 24 24\"\n          fill=\"none\"\n          stroke=\"var(--ink-3)\"\n          strokeWidth=\"2.2\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          className=\"transition-transform duration-300\"\n          style={{ transform: expanded ? \"rotate(180deg)\" : \"rotate(0)\" }}\n        >\n          <path d=\"M6 9l6 6 6-6\" />\n        </svg>\n      </button>\n\n      {/* expandable trace */}\n      <div\n        className=\"grid transition-[grid-template-rows,opacity] duration-400\"\n        style={{\n          gridTemplateRows: expanded ? \"1fr\" : \"0fr\",\n          opacity: expanded ? 1 : 0,\n          transitionTimingFunction: \"cubic-bezier(0.23, 1, 0.32, 1)\",\n        }}\n      >\n        <div className=\"overflow-hidden\">\n          <div className=\"relative mt-1 ml-[5px] pl-4\">\n            <span\n              aria-hidden\n              className=\"absolute left-[3px] w-px bg-line\"\n              style={{\n                top: -8,\n                height: lineHeight ? lineHeight - 2 : 0,\n                transition: \"height 500ms cubic-bezier(0.23,1,0.32,1)\",\n              }}\n            />\n            <div ref={traceRef} className=\"flex flex-col gap-1 py-1\">\n              {v.query && (\n                <div\n                  className={cn(\"flex h-6 items-center gap-2 px-1.5\", expanded && \"animate-thinking-state-query-in\")}\n                >\n                  <svg\n                    width=\"14\"\n                    height=\"14\"\n                    viewBox=\"0 0 24 24\"\n                    fill=\"none\"\n                    stroke=\"var(--ink-3)\"\n                    strokeWidth=\"2\"\n                    strokeLinecap=\"round\"\n                    className=\"shrink-0\"\n                  >\n                    <circle cx=\"11\" cy=\"11\" r=\"7\" />\n                    <path d=\"M21 21l-4.3-4.3\" />\n                  </svg>\n                  <span className=\"text-[12.5px] text-ink-2\">{v.query}</span>\n                </div>\n              )}\n              {v.rows.slice(0, visible).map((row, i) => {\n                const content = (\n                  <>\n                    {variant === \"Search\" && <Dot tone={TONES[i % 3]} />}\n                    {variant === \"Steps\" &&\n                      (i < visible - 1 || !working ? (\n                        <svg\n                          width=\"14\"\n                          height=\"14\"\n                          viewBox=\"0 0 24 24\"\n                          fill=\"none\"\n                          stroke=\"var(--ink-3)\"\n                          strokeWidth=\"2.5\"\n                          strokeLinecap=\"round\"\n                          strokeLinejoin=\"round\"\n                          className=\"shrink-0\"\n                        >\n                          <path d=\"M20 6L9 17l-5-5\" />\n                        </svg>\n                      ) : (\n                        <span className=\"size-3 shrink-0 animate-thinking-state-spin rounded-full border-[1.5px] border-line-strong border-t-ink-2\" />\n                      ))}\n                    <span\n                      className={cn(\n                        \"min-w-0 truncate text-[12.5px]\",\n                        variant === \"Reasoning\"\n                          ? \"whitespace-normal leading-relaxed text-ink-2\"\n                          : \"font-medium text-ink\",\n                        variant === \"Search\" &&\n                          \"relative inline-block after:absolute after:inset-x-0 after:-bottom-px after:h-px after:origin-left after:scale-x-0 after:bg-current after:transition-transform after:duration-[280ms] after:ease-link after:content-[''] group-hover/source:after:scale-x-100 group-focus-visible/source:after:scale-x-100\",\n                      )}\n                    >\n                      {row.primary}\n                    </span>\n                    {row.secondary && (\n                      <span className={`shrink-0 text-[11.5px] text-ink-3 ${row.mono ? \"font-mono\" : \"\"}`}>\n                        {row.secondary}\n                      </span>\n                    )}\n                    {row.add !== undefined && (\n                      <span className=\"shrink-0 font-mono text-[11px] tabular-nums\">\n                        <span className=\"text-green\">+{row.add}</span> <span className=\"text-red\">−{row.del}</span>\n                      </span>\n                    )}\n                  </>\n                );\n                const rowClass =\n                  \"flex min-h-7 w-full animate-thinking-state-row-in items-center gap-2 rounded-[6px] px-1.5 py-0.5 text-left\";\n                const animation = { animationDelay: `${i * 120}ms` };\n\n                if (variant === \"Search\") {\n                  return (\n                    <a\n                      key={row.primary}\n                      href={row.href}\n                      target=\"_blank\"\n                      rel=\"noreferrer\"\n                      className={`group/source ${rowClass} transition-colors duration-150 hover:bg-hover`}\n                      style={animation}\n                    >\n                      {content}\n                    </a>\n                  );\n                }\n\n                if (variant === \"Coding\") {\n                  const selected = selectedTool === row.primary;\n                  return (\n                    <button\n                      key={row.primary}\n                      type=\"button\"\n                      aria-pressed={selected}\n                      onClick={() => setSelectedTool(selected ? null : row.primary)}\n                      className={`${rowClass} transition-colors duration-150 ${selected ? \"bg-inset\" : \"hover:bg-hover\"}`}\n                      style={animation}\n                    >\n                      {content}\n                    </button>\n                  );\n                }\n\n                return (\n                  <div key={row.primary} className={rowClass} style={animation}>\n                    {content}\n                  </div>\n                );\n              })}\n              {variant === \"Search\" && stage >= 3 && (\n                <span className=\"animate-thinking-state-more-in text-[12px] text-ink-3\">+7 more</span>\n              )}\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/iris/thinking-state/thinking-state.tsx"
    }
  ],
  "docs": "Docs and live preview: https://irisui.dev/components/thinking-state",
  "meta": {
    "kind": "component",
    "group": "AI interface",
    "export": "ThinkingState",
    "docs": "https://irisui.dev/components/thinking-state"
  }
}
