{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "thinking",
  "title": "Thinking",
  "description": "Expandable AI trace for step, reasoning, search, and coding progress with reusable row data and reduced-motion support.",
  "registryDependencies": [
    "https://beadkit.dev/r/ai-core.json"
  ],
  "files": [
    {
      "path": "components/ai/ThinkingState.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useId, useRef, useState, type CSSProperties, type ReactNode } from \"react\";\nimport styles from \"./ai-state.module.css\";\nimport { safeExternalHref } from \"./safeExternalHref\";\n\nexport type ThinkingVariant = \"steps\" | \"reasoning\" | \"search\" | \"coding\";\nexport type ThinkingStatus = \"working\" | \"complete\" | \"error\" | \"unavailable\";\n\nexport interface ThinkingRow {\n  label: string;\n  detail?: string;\n  href?: string;\n  additions?: number;\n  deletions?: number;\n}\n\nexport interface ThinkingStateProps {\n  variant?: ThinkingVariant;\n  status?: ThinkingStatus;\n  rows: readonly ThinkingRow[];\n  query?: string;\n  activeLabel?: string;\n  completeLabel?: string;\n  errorLabel?: string;\n  unavailableLabel?: string;\n  visibleCount?: number;\n  moreCount?: number;\n  defaultExpanded?: boolean;\n  className?: string;\n}\n\nconst sourceTones = [\n  \"bg-accent text-accent-foreground\",\n  \"bg-success text-white\",\n  \"bg-primary text-primary-foreground\",\n];\n\nfunction CheckIcon() {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      width=\"14\"\n      height=\"14\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth=\"2.5\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      className=\"text-muted-foreground shrink-0\"\n    >\n      <path d=\"M20 6 9 17l-5-5\" />\n    </svg>\n  );\n}\n\nfunction SearchMark({ index }: { index: number }) {\n  return (\n    <span\n      aria-hidden=\"true\"\n      className={`flex size-3.5 shrink-0 items-center justify-center rounded-full ${\n        sourceTones[index % sourceTones.length]\n      }`}\n    >\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\nexport function ThinkingState({\n  variant = \"steps\",\n  status = \"working\",\n  rows,\n  query,\n  activeLabel = variant === \"search\" ? \"Searching\" : variant === \"coding\" ? \"Running tools\" : \"Thinking\",\n  completeLabel = variant === \"search\" ? \"Search complete\" : variant === \"coding\" ? \"Tools complete\" : \"Thought through it\",\n  errorLabel = \"Unable to continue\",\n  unavailableLabel = \"Unavailable\",\n  visibleCount = rows.length,\n  moreCount = 0,\n  defaultExpanded = true,\n  className = \"\",\n}: ThinkingStateProps) {\n  const [expanded, setExpanded] = useState(defaultExpanded);\n  const [selectedTool, setSelectedTool] = useState<string | null>(null);\n  const [liveStatus, setLiveStatus] = useState(\"\");\n  const disclosureRef = useRef<HTMLButtonElement>(null);\n  const contentRef = useRef<HTMLDivElement>(null);\n  const previousStatusRef = useRef(status);\n  const contentId = useId();\n  const working = status === \"working\";\n  const statusLabel =\n    status === \"working\"\n      ? activeLabel\n      : status === \"complete\"\n        ? completeLabel\n        : status === \"error\"\n          ? errorLabel\n          : unavailableLabel;\n  const visibleRows = rows.slice(0, Math.max(0, visibleCount));\n\n  useEffect(() => {\n    if (previousStatusRef.current !== status) {\n      previousStatusRef.current = status;\n      setLiveStatus(statusLabel);\n    }\n  }, [status, statusLabel]);\n\n  function toggleExpanded() {\n    if (expanded && contentRef.current?.contains(document.activeElement)) {\n      disclosureRef.current?.focus();\n    }\n\n    setExpanded((value) => !value);\n  }\n\n  function rowContent(row: ThinkingRow, index: number): ReactNode {\n    const currentStep = working && index === visibleRows.length - 1;\n\n    return (\n      <>\n        {variant === \"search\" ? <SearchMark index={index} /> : null}\n        {variant === \"steps\" ? (\n          currentStep ? (\n            <span\n              aria-hidden=\"true\"\n              className={`${styles.spinner} border-muted-foreground/30 size-3 shrink-0 rounded-full border-[1.5px] border-t-foreground`}\n            />\n          ) : (\n            <CheckIcon />\n          )\n        ) : null}\n        <span\n          className={`min-w-0 flex-1 text-[12.5px] ${\n            variant === \"reasoning\"\n              ? \"text-muted-foreground whitespace-normal leading-relaxed\"\n              : \"truncate font-medium\"\n          }`}\n        >\n          {row.label}\n        </span>\n        {row.detail ? (\n          <span className={`text-muted-foreground shrink-0 text-[11.5px] ${variant === \"coding\" ? \"font-mono\" : \"\"}`}>\n            {row.detail}\n          </span>\n        ) : null}\n        {row.additions !== undefined ? (\n          <span className=\"shrink-0 font-mono text-[11px] tabular-nums\">\n            <span className=\"text-success\">+{row.additions}</span>{\" \"}\n            <span className=\"text-destructive\">−{row.deletions ?? 0}</span>\n          </span>\n        ) : null}\n      </>\n    );\n  }\n\n  return (\n    <div\n      data-ai-state=\"thinking\"\n      data-variant={variant}\n      aria-busy={working}\n      className={`flex w-full max-w-md flex-col ${className}`}\n    >\n      <button\n        ref={disclosureRef}\n        type=\"button\"\n        aria-expanded={expanded}\n        aria-controls={contentId}\n        onClick={toggleExpanded}\n        className={`hover:bg-muted flex min-h-11 w-fit items-center gap-2 rounded-lg text-start transition-colors duration-150 ${styles.disclosure}`}\n      >\n        <svg\n          aria-hidden=\"true\"\n          width=\"16\"\n          height=\"16\"\n          viewBox=\"0 0 24 24\"\n          className={working ? \"fill-foreground\" : \"fill-muted-foreground\"}\n        >\n          <path d=\"m12 2 2.4 7.2L22 12l-7.6 2.8L12 22l-2.4-7.2L2 12l7.6-2.8z\" />\n        </svg>\n        <span\n          data-label={working ? activeLabel : undefined}\n          className={`${working ? styles.shimmer : styles.fadeIn} text-[13px] font-medium whitespace-nowrap`}\n        >\n          {statusLabel}\n        </span>\n        <svg\n          aria-hidden=\"true\"\n          width=\"14\"\n          height=\"14\"\n          viewBox=\"0 0 24 24\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth=\"2.2\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          className={`${styles.motion} text-muted-foreground transition-transform`}\n          style={{ transform: expanded ? \"rotate(180deg)\" : \"rotate(0deg)\" }}\n        >\n          <path d=\"m6 9 6 6 6-6\" />\n        </svg>\n      </button>\n\n      <div\n        ref={contentRef}\n        id={contentId}\n        aria-hidden={!expanded}\n        inert={!expanded}\n        className={`${styles.motion} grid transition-[grid-template-rows,opacity]`}\n        style={{ gridTemplateRows: expanded ? \"1fr\" : \"0fr\", opacity: expanded ? 1 : 0 }}\n      >\n        <div className=\"overflow-hidden\">\n          <div className={`border-border relative ${styles.contentBorder}`}>\n            <div className=\"flex flex-col gap-1 py-1\">\n              {query ? (\n                <div className={`${styles.fadeUp} text-muted-foreground flex min-h-7 items-center gap-2 px-1.5 text-[12.5px]`}>\n                  <svg aria-hidden=\"true\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\">\n                    <circle cx=\"11\" cy=\"11\" r=\"7\" />\n                    <path d=\"m21 21-4.3-4.3\" />\n                  </svg>\n                  <span>{query}</span>\n                </div>\n              ) : null}\n\n              {visibleRows.map((row, index) => {\n                const shared = `flex min-h-8 w-full items-center gap-2 rounded-md px-1.5 py-1 text-start ${styles.fadeUp}`;\n                const animationStyle = { animationDelay: `${index * 90}ms` } as CSSProperties;\n                const href = safeExternalHref(row.href);\n\n                if (variant === \"search\" && href) {\n                  return (\n                    <a\n                      key={`${row.label}-${index}`}\n                      href={href}\n                      target=\"_blank\"\n                      rel=\"noreferrer\"\n                      className={`${shared} hover:bg-muted transition-colors duration-150`}\n                      style={animationStyle}\n                    >\n                      {rowContent(row, index)}\n                    </a>\n                  );\n                }\n\n                if (variant === \"coding\") {\n                  const selected = selectedTool === row.label;\n                  return (\n                    <button\n                      key={`${row.label}-${index}`}\n                      type=\"button\"\n                      aria-pressed={selected}\n                      onClick={() => setSelectedTool(selected ? null : row.label)}\n                      className={`${shared} hover:bg-muted transition-colors duration-150 ${selected ? \"bg-muted\" : \"\"}`}\n                      style={animationStyle}\n                    >\n                      {rowContent(row, index)}\n                    </button>\n                  );\n                }\n\n                return (\n                  <div key={`${row.label}-${index}`} className={shared} style={animationStyle}>\n                    {rowContent(row, index)}\n                  </div>\n                );\n              })}\n\n              {moreCount > 0 ? (\n                <span className={`${styles.fadeIn} text-muted-foreground px-1.5 text-xs`}>+{moreCount} more</span>\n              ) : null}\n            </div>\n          </div>\n        </div>\n      </div>\n      <div aria-live=\"polite\" aria-atomic=\"true\" className=\"sr-only\">\n        {liveStatus}\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/ai/ThinkingState.tsx"
    }
  ],
  "type": "registry:component"
}
