{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"thinking-state","title":"Thinking","description":"Expandable traces — steps, reasoning, search, coding.","dependencies":[],"registryDependencies":["https://ward.so/r/foundation.json"],"files":[{"path":"registry/ward/ThinkingState.tsx","content":"\"use client\";\n/*\n * MIT License\n *\n * Copyright (c) 2026 Shane Levine\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nimport \"./styles/foundation.css\";\n\nimport { useEffect, useLayoutEffect, useRef, useState, type ReactNode } from \"react\";\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\nconst VARIANTS: Record<\n  string,\n  { active: string; done: string; rows: Row[]; query?: string }\n> = {\n  Steps: {\n    active: \"Thinking\",\n    done: \"Thought for 4 seconds\",\n    rows: [\n      { primary: \"Reading project briefs\" },\n      { primary: \"Scanning supplier lists\" },\n      { primary: \"Comparing tasting notes\", secondary: \"6 projects\" },\n      { primary: \"Writing the work report\" },\n    ],\n  },\n  Reasoning: {\n    active: \"Thinking\",\n    done: \"Thought for 4 seconds\",\n    rows: [\n      { primary: \"Summer demand spikes for stone-fruit projects — peach and apricot lead.\" },\n      { primary: \"I should check asset inventory before promoting a design-bowl special.\" },\n    ],\n  },\n  Search: {\n    active: \"Searching the web\",\n    done: \"Searched the web\",\n    query: \"best design asset supplier\",\n    rows: [\n      { primary: \"Reference One\", secondary: \"example.com\", href: \"https://example.com/reference-one\" },\n      { primary: \"Reference Two\", secondary: \"example.com\", href: \"https://example.com/reference-two\" },\n      { primary: \"Reference Three\", secondary: \"example.com\", href: \"https://example.com/reference-three\" },\n    ],\n  },\n  Coding: {\n    active: \"Running tools\",\n    done: \"Ran 3 tools\",\n    rows: [\n      { primary: \"Read\", secondary: \"projects.ts\", mono: true },\n      { primary: \"Edit\", secondary: \"PlanSchedule.tsx\", mono: true, add: 74, del: 41 },\n      { primary: \"Run\", secondary: \"npm run freeze\", 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-highlight\", \"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-control px-1.5 py-1\n          transition-colors duration-100 hover:bg-hover-2\"\n      >\n        {icon ? (\n          <span className=\"flex shrink-0 transition-colors duration-200\" style={{ color: working ? \"var(--ink-2)\" : \"var(--ink-3)\" }}>\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        <span role=\"status\" className=\"contents\">\n          {working ? (\n            <span\n              className=\"bg-clip-text text-[13px] font-medium whitespace-nowrap text-transparent\"\n              style={{\n                backgroundImage:\n                  \"linear-gradient(90deg, var(--ink-3) 35%, var(--ink) 50%, var(--ink-3) 65%)\",\n                backgroundSize: \"200% 100%\",\n                animation: \"shimmer-text 1.4s linear infinite\",\n              }}\n            >\n              {v.active}\n            </span>\n          ) : (\n            <span\n              className=\"text-[13px] font-medium whitespace-nowrap text-ink-2\"\n              style={{ animation: \"fade-in 350ms ease-out both\" }}\n            >\n              {v.done}\n            </span>\n          )}\n        </span>\n        <svg\n          width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"var(--ink-3)\" strokeWidth=\"2.2\" strokeLinecap=\"round\" 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={{ top: -8, height: lineHeight ? lineHeight - 2 : 0, transition: \"height 500ms cubic-bezier(0.23,1,0.32,1)\" }}\n            />\n            <div ref={traceRef} className=\"flex flex-col gap-1 py-1\">\n            {v.query && (\n              <div className=\"flex h-6 items-center gap-2 px-1.5\" style={{ animation: expanded ? \"fade-up 300ms cubic-bezier(0.23,1,0.32,1) both\" : undefined }}>\n                <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"var(--ink-3)\" strokeWidth=\"2\" strokeLinecap=\"round\" className=\"shrink-0\">\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 width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"var(--ink-3)\" strokeWidth=\"2.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className=\"shrink-0\">\n                      <path d=\"M20 6L9 17l-5-5\" />\n                    </svg>\n                  ) : (\n                    <span className=\"size-3 shrink-0 rounded-full border-[1.5px] border-line-strong border-t-ink-2\" style={{ animation: \"spin 700ms linear infinite\" }} />\n                  )\n                )}\n                <span className={`min-w-0 truncate text-[12.5px] ${variant === \"Reasoning\" ? \"whitespace-normal leading-relaxed text-ink-2\" : \"font-medium text-ink\"} ${variant === \"Search\" ? \"animated-underline\" : \"\"}`}>\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>{\" \"}\n                    <span className=\"text-red\">−{row.del}</span>\n                  </span>\n                )}\n                </>\n              );\n              const rowClass = \"flex min-h-7 w-full items-center gap-2 rounded-[6px] px-1.5 py-0.5 text-left\";\n              const animation = { animation: `fade-up 320ms cubic-bezier(0.23,1,0.32,1) ${i * 120}ms both` };\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={`${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=\"text-[12px] text-ink-3\" style={{ animation: \"fade-in 300ms ease-out both\" }}>\n                +7 more\n              </span>\n            )}\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n","type":"registry:component","target":"components/ward/ThinkingState.tsx"}],"meta":{"variants":["Steps","Reasoning","Search","Coding"],"version":"1.1.0","source":"https://github.com/slev12397/beautiful-ui/blob/44a274e598395ab61e7c96c26fda2758780253b7/components/primitives/ThinkingState.tsx","access":"free"},"categories":["ai"],"type":"registry:component"}