{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "claude-spinner",
  "title": "Claude Spinner",
  "description": "A reverse-engineered recreation of the Claude Code CLI's terminal spinner — animated glyph, status verb, elapsed time, and token counter.",
  "files": [
    {
      "path": "registry/components/claude-spinner/claude-spinner.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useMemo, useRef, useState } from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nimport \"./claude-spinner.css\"\n\n// Modeled after the Claude Code CLI binary (@anthropic-ai/claude-code).\n// Glyph set for non-Ghostty terminals — Ghostty gets \"✳\" instead of \"*\".\nconst GLYPHS = [\"·\", \"✢\", \"*\", \"✶\", \"✻\", \"✽\"]\nconst CYCLE_MS = 2000 // one full ease-in/ease-out sweep across the glyph set\nconst TICK_MS = 100 // real CLI ticks state at 100ms (50ms while \"requesting\")\n\n// Colors pulled from the binary's blend targets.\nconst COLOR_BASE = { r: 217, g: 119, b: 87 } // Claude coral accent\nconst COLOR_WARNING = { r: 214, g: 158, b: 46 } // amber — long-running turn\nconst COLOR_ERROR = { r: 171, g: 43, b: 63 } // red — stalled/network issue\n\n// Full verb list extracted from the binary's spinnerVerbs array.\nexport const VERBS = [\n  \"Accomplishing\", \"Actioning\", \"Actualizing\", \"Architecting\", \"Baking\", \"Beaming\", \"Beboppin'\",\n  \"Befuddling\", \"Billowing\", \"Blanching\", \"Bloviating\", \"Boogieing\", \"Boondoggling\", \"Booping\",\n  \"Bootstrapping\", \"Brewing\", \"Bunning\", \"Burrowing\", \"Calculating\", \"Canoodling\", \"Caramelizing\",\n  \"Cascading\", \"Catapulting\", \"Cerebrating\", \"Channeling\", \"Channelling\", \"Choreographing\", \"Churning\",\n  \"Clauding\", \"Coalescing\", \"Cogitating\", \"Combobulating\", \"Composing\", \"Computing\", \"Concocting\",\n  \"Considering\", \"Contemplating\", \"Cooking\", \"Crafting\", \"Creating\", \"Crunching\", \"Crystallizing\",\n  \"Cultivating\", \"Deciphering\", \"Deliberating\", \"Determining\", \"Dilly-dallying\", \"Discombobulating\",\n  \"Doing\", \"Doodling\", \"Drizzling\", \"Ebbing\", \"Effecting\", \"Elucidating\", \"Embellishing\", \"Enchanting\",\n  \"Envisioning\", \"Fermenting\", \"Fiddle-faddling\", \"Finagling\", \"Flambéing\", \"Flibbertigibbeting\",\n  \"Flowing\", \"Flummoxing\", \"Fluttering\", \"Forging\", \"Forming\", \"Frolicking\", \"Frosting\", \"Gallivanting\",\n  \"Galloping\", \"Garnishing\", \"Generating\", \"Gesticulating\", \"Germinating\", \"Gitifying\", \"Grooving\",\n  \"Gusting\", \"Harmonizing\", \"Hashing\", \"Hatching\", \"Herding\", \"Honking\", \"Hullaballooing\",\n  \"Hyperspacing\", \"Ideating\", \"Imagining\", \"Improvising\", \"Incubating\", \"Inferring\", \"Infusing\",\n  \"Ionizing\", \"Jitterbugging\", \"Julienning\", \"Kneading\", \"Leavening\", \"Levitating\", \"Lollygagging\",\n  \"Manifesting\", \"Marinating\", \"Meandering\", \"Metamorphosing\", \"Misting\", \"Moonwalking\", \"Moseying\",\n  \"Mulling\", \"Mustering\", \"Musing\", \"Nebulizing\", \"Nesting\", \"Newspapering\", \"Noodling\", \"Nucleating\",\n  \"Orbiting\", \"Orchestrating\", \"Osmosing\", \"Perambulating\", \"Percolating\", \"Perusing\", \"Philosophising\",\n  \"Photosynthesizing\", \"Pollinating\", \"Pondering\", \"Pontificating\", \"Pouncing\", \"Precipitating\",\n  \"Prestidigitating\", \"Processing\", \"Proofing\", \"Propagating\", \"Puttering\", \"Puzzling\", \"Quantumizing\",\n  \"Razzle-dazzling\", \"Razzmatazzing\", \"Recombobulating\", \"Reticulating\", \"Roosting\", \"Ruminating\",\n  \"Sautéing\", \"Scampering\", \"Schlepping\", \"Scurrying\", \"Seasoning\", \"Shenaniganing\", \"Shimmying\",\n  \"Simmering\", \"Skedaddling\", \"Sketching\", \"Slithering\", \"Smooshing\", \"Sock-hopping\", \"Spelunking\",\n  \"Spinning\", \"Sprouting\", \"Stewing\", \"Sublimating\", \"Swirling\", \"Swooping\", \"Symbioting\",\n  \"Synthesizing\", \"Tempering\", \"Thinking\", \"Thundering\", \"Tinkering\", \"Tomfoolering\", \"Topsy-turvying\",\n  \"Transfiguring\", \"Transmuting\", \"Twisting\", \"Undulating\", \"Unfurling\", \"Unravelling\", \"Vibing\",\n  \"Waddling\", \"Wandering\", \"Warping\", \"Whatchamacalliting\", \"Whirlpooling\", \"Whirring\", \"Whisking\",\n  \"Wibbling\", \"Working\", \"Wrangling\", \"Zesting\", \"Zigzagging\",\n]\n\n// Progressive sub-status once a turn drags on, independent of the random verb.\nconst SUB_STATUS_THRESHOLDS: [number, string][] = [\n  [45000, \"almost done thinking\"],\n  [30000, \"thinking some more\"],\n  [20000, \"thinking more\"],\n  [10000, \"still thinking\"],\n]\n\nfunction subStatusFor(ms: number) {\n  for (const [threshold, text] of SUB_STATUS_THRESHOLDS) {\n    if (ms >= threshold) return text\n  }\n  return \"thinking\"\n}\n\nfunction formatElapsed(totalSeconds: number) {\n  const minutes = Math.floor(totalSeconds / 60)\n  const seconds = totalSeconds % 60\n  return minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`\n}\n\n// Approximates the CLI's observed output token rate, used to auto-advance the\n// token counter when `tokens` isn't controlled by the caller.\nconst TOKENS_PER_SEC = 16.7\n\nfunction formatTokenCount(count: number) {\n  return count >= 1000 ? `${(count / 1000).toFixed(1)}k` : `${count}`\n}\n\nfunction ease(phase: number) {\n  return (1 - Math.cos(2 * Math.PI * phase)) / 2\n}\n\nfunction lerpColor(a: typeof COLOR_BASE, b: typeof COLOR_BASE, t: number) {\n  return {\n    r: Math.round(a.r + (b.r - a.r) * t),\n    g: Math.round(a.g + (b.g - a.g) * t),\n    b: Math.round(a.b + (b.b - a.b) * t),\n  }\n}\n\nfunction rgb(c: { r: number; g: number; b: number }) {\n  return `rgb(${c.r},${c.g},${c.b})`\n}\n\nfunction usePrefersReducedMotion() {\n  const [reduced, setReduced] = useState(\n    () => typeof window !== \"undefined\" && window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n  )\n  useEffect(() => {\n    const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\")\n    const handler = () => setReduced(mq.matches)\n    mq.addEventListener(\"change\", handler)\n    return () => mq.removeEventListener(\"change\", handler)\n  }, [])\n  return reduced\n}\n\nexport interface ClaudeSpinnerProps {\n  /** Overrides the randomly chosen status verb, e.g. \"Reading files\". */\n  label?: string\n  /** Blends the glyph color toward red, as the CLI does on a network stall. */\n  stalled?: boolean\n  /** Hides the elapsed-time counter. */\n  hideElapsed?: boolean\n  /** Overrides the auto-incrementing \"↓ Nk tokens\" counter. */\n  tokens?: number\n  /** Hides the \"↓ Nk tokens\" counter. */\n  hideTokens?: boolean\n  /** Hides the progressive \"still thinking\" sub-status. */\n  hideSubStatus?: boolean\n  /** Force reduced-motion behavior regardless of the OS setting. */\n  reducedMotion?: boolean\n  className?: string\n}\n\nexport function ClaudeSpinner({\n  label,\n  stalled = false,\n  hideElapsed = false,\n  tokens,\n  hideTokens = false,\n  hideSubStatus = false,\n  reducedMotion,\n  className,\n}: ClaudeSpinnerProps) {\n  const verb = useMemo(() => label ?? VERBS[Math.floor(Math.random() * VERBS.length)], [label])\n  const startRef = useRef(Date.now())\n  const osReducedMotion = usePrefersReducedMotion()\n  const isReducedMotion = reducedMotion ?? osReducedMotion\n\n  const [elapsedMs, setElapsedMs] = useState(0)\n  const [elapsedSec, setElapsedSec] = useState(0)\n\n  useEffect(() => {\n    const timer = setInterval(() => {\n      setElapsedMs(Date.now() - startRef.current)\n    }, TICK_MS)\n    return () => clearInterval(timer)\n  }, [])\n\n  useEffect(() => {\n    setElapsedSec(Math.floor(elapsedMs / 1000))\n  }, [elapsedMs])\n\n  const thinkingIntensity = Math.min(Math.max((elapsedMs - 10000) / 10000, 0), 1)\n  const target = stalled ? COLOR_ERROR : thinkingIntensity > 0 ? COLOR_WARNING : COLOR_BASE\n  const intensity = stalled ? 1 : thinkingIntensity\n  const color = rgb(lerpColor(COLOR_BASE, target, intensity))\n\n  const phase = (elapsedMs % CYCLE_MS) / CYCLE_MS\n  const frameIndex = isReducedMotion ? 0 : Math.round(ease(phase) * (GLYPHS.length - 1))\n  const glyph = isReducedMotion ? \"●\" : GLYPHS[frameIndex]\n\n  const subStatus = subStatusFor(elapsedMs)\n  const tokenCount = tokens ?? Math.round(elapsedSec * TOKENS_PER_SEC)\n\n  const metaParts: string[] = []\n  if (!hideElapsed) metaParts.push(formatElapsed(elapsedSec))\n  if (!hideTokens) metaParts.push(`↓ ${formatTokenCount(tokenCount)} tokens`)\n  if (!hideSubStatus) metaParts.push(subStatus)\n\n  return (\n    <div className={cn(\"cc-spinner\", className)} role=\"status\" aria-live=\"polite\">\n      <span\n        className={cn(\"cc-spinner__glyph\", isReducedMotion && \"cc-spinner__glyph--pulse\")}\n        style={{ color }}\n      >\n        {glyph}\n      </span>\n      <span className=\"cc-spinner__label\" style={{ color }}>\n        {verb}…\n      </span>\n      {metaParts.length > 0 && (\n        <span className=\"cc-spinner__meta\">({metaParts.join(\" · \")})</span>\n      )}\n    </div>\n  )\n}\n\nexport default ClaudeSpinner\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/components/claude-spinner/claude-spinner.css",
      "content": ".cc-spinner {\n  display: inline-flex;\n  align-items: center;\n  gap: 0.5em;\n  font-family: ui-monospace, \"SF Mono\", \"Cascadia Code\", Menlo, Consolas, monospace;\n  font-size: 14px;\n}\n\n.cc-spinner__glyph {\n  display: inline-block;\n  width: 1em;\n  text-align: center;\n  font-weight: 600;\n}\n\n.cc-spinner__glyph--pulse {\n  animation: cc-spinner-pulse 2s ease-in-out infinite;\n}\n\n.cc-spinner__label {\n  font-weight: 500;\n}\n\n.cc-spinner__meta {\n  color: var(--cc-spinner-muted, #71717a);\n}\n\n@keyframes cc-spinner-pulse {\n  0%, 100% { opacity: 0.35; }\n  50% { opacity: 1; }\n}\n",
      "type": "registry:file",
      "target": "components/ui/claude-spinner.css"
    }
  ],
  "categories": [
    "effects"
  ],
  "type": "registry:ui"
}