{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "thinking-state",
  "title": "Thinking State",
  "description": "A tiny pixel animation beside a word, with thinking, debugging and more states, plus done and error.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/clover-ui/thinking-state.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type ThinkingStateKind =\n  \"thinking\" | \"analyzing\" | \"debugging\" | \"searching\" | \"building\" | \"pondering\";\n\nexport type ThinkingStatePixel = \"square\" | \"circle\";\n\nexport type ThinkingStateStatus = \"active\" | \"complete\" | \"error\";\n\nexport interface ThinkingStateProps extends Omit<React.ComponentProps<\"span\">, \"color\"> {\n  state?: ThinkingStateKind;\n  status?: ThinkingStateStatus;\n  color?: string;\n  pixel?: ThinkingStatePixel;\n  grid?: boolean;\n  speed?: number;\n  shimmer?: boolean;\n}\n\nconst GRID = 5;\nconst CENTER = (GRID - 1) / 2;\n\nconst STATES: Record<ThinkingStateKind, { label: string; light: string; dark: string }> = {\n  thinking: { label: \"Thinking\", light: \"#16a34a\", dark: \"#4ade80\" },\n  analyzing: { label: \"Analyzing\", light: \"#0284c7\", dark: \"#7dd3fc\" },\n  debugging: { label: \"Debugging\", light: \"#2563eb\", dark: \"#60a5fa\" },\n  searching: { label: \"Searching\", light: \"#7c3aed\", dark: \"#a78bfa\" },\n  building: { label: \"Building\", light: \"#ea580c\", dark: \"#fb923c\" },\n  pondering: { label: \"Pondering\", light: \"#db2777\", dark: \"#f472b6\" },\n};\n\nconst STATUS_COLORS: Record<\n  Exclude<ThinkingStateStatus, \"active\">,\n  { light: string; dark: string }\n> = {\n  complete: { light: \"#16a34a\", dark: \"#4ade80\" },\n  error: { light: \"#dc2626\", dark: \"#f87171\" },\n};\n\nconst ERROR_CELLS = [6, 12, 18, 8, 16];\n\nconst clamp = (value: number) => Math.min(1, Math.max(0, value));\n\nconst ROUND = Array.from(\n  { length: GRID * GRID },\n  (_, i) => Math.hypot((i % GRID) - CENTER, Math.floor(i / GRID) - CENTER) <= CENTER + 0.4,\n);\n\nconst RING = (() => {\n  const cells: number[] = [];\n  for (let x = 0; x < GRID; x++) cells.push(x);\n  for (let y = 1; y < GRID; y++) cells.push(y * GRID + GRID - 1);\n  for (let x = GRID - 2; x >= 0; x--) cells.push((GRID - 1) * GRID + x);\n  for (let y = GRID - 2; y >= 1; y--) cells.push(y * GRID);\n  return cells;\n})();\n\ntype CellPaint = (x: number, y: number, t: number) => number;\n\nconst CELL_PAINTS: Record<Exclude<ThinkingStateKind, \"thinking\">, CellPaint> = {\n  searching: (x, y, t) => {\n    let level = 0;\n    for (let k = 0; k < 5; k++) {\n      const at = t * 1.6 - k * 0.1;\n      const px = CENTER + Math.sin(at * 1.3) * 1.8;\n      const py = CENTER + Math.sin(at * 2.6) * 1.7;\n      level = Math.max(level, clamp(1 - Math.hypot(x - px, y - py) / 0.8) * (1 - k / 5));\n    }\n    return level;\n  },\n  building: (x, y, t) => {\n    const filled = (t * 8) % (GRID * GRID + 8);\n    const order = (GRID - 1 - y) * GRID + (y % 2 ? GRID - 1 - x : x);\n    const fade = filled > GRID * GRID ? clamp((GRID * GRID + 8 - filled) / 5) : 1;\n    return order < filled - 1 ? 0.8 * fade : order < filled ? 1 : 0;\n  },\n  pondering: (x, y, t) => {\n    const distance = Math.hypot(x - CENTER, y - CENTER);\n    let level = 0;\n    for (let k = 0; k < 3; k++) {\n      const radius = (t * 1.8 + k * 1.15) % 3.4;\n      level = Math.max(level, clamp(1 - Math.abs(distance - radius) / 0.55) * (1 - radius / 3.4));\n    }\n    return level;\n  },\n  analyzing: (x, y, t) => {\n    const dx = x - CENTER;\n    const dy = y - CENTER;\n    const onRing = clamp(1 - Math.abs(Math.hypot(dx, dy) - 1.8) / 0.7);\n    const angle = Math.atan2(dy, dx);\n    const sweep = (Math.cos(angle - t * 4) + 1) / 2;\n    const echo = (Math.cos(angle - t * 4 + Math.PI) + 1) / 2;\n    return onRing * Math.max(sweep ** 4, echo ** 6 * 0.6);\n  },\n  debugging: (x, y, t) => {\n    const wave = ((t * 5) % 14) - 3;\n    const diagonal = x + (GRID - 1 - y);\n    return clamp(1 - Math.abs(diagonal - wave) / 2.4);\n  },\n};\n\nfunction paint(state: ThinkingStateKind, t: number, out: Float32Array) {\n  out.fill(0);\n\n  if (state === \"thinking\") {\n    const head = (t * 12) % RING.length;\n    RING.forEach((cell, index) => {\n      const behind = (head - index + RING.length) % RING.length;\n      out[cell] = clamp(1 - behind / 6);\n    });\n    return;\n  }\n\n  const cellPaint = CELL_PAINTS[state];\n  for (let y = 0; y < GRID; y++) {\n    for (let x = 0; x < GRID; x++) out[y * GRID + x] = cellPaint(x, y, t);\n  }\n}\n\nconst SHIMMER_CSS =\n  \"@keyframes thinking-state-shimmer{0%{background-position:100% 0}100%{background-position:-100% 0}}\";\n\nconst loopKeyframes = (name: string, tone: \"light\" | \"dark\") => {\n  const colors = Object.values(STATES).map((preset) => preset[tone]);\n  const stops = [...colors, colors[0]]\n    .map(\n      (value, index, all) =>\n        `${((index / (all.length - 1)) * 100).toFixed(2)}%{--thinking-color:${value}}`,\n    )\n    .join(\"\");\n  return `@keyframes ${name}{${stops}}`;\n};\n\nconst LOOP_CSS = `@property --thinking-color{syntax:\"<color>\";inherits:true;initial-value:transparent}${loopKeyframes(\"thinking-state-loop-light\", \"light\")}${loopKeyframes(\"thinking-state-loop-dark\", \"dark\")}`;\n\nfunction ThinkingState({\n  state = \"thinking\",\n  status = \"active\",\n  color,\n  pixel = \"square\",\n  grid = true,\n  speed = 1,\n  shimmer = true,\n  className,\n  children,\n  style,\n  ...props\n}: ThinkingStateProps) {\n  const reduceMotion = useReducedMotion();\n  const cellsRef = React.useRef<(SVGRectElement | null)[]>([]);\n  const levelsRef = React.useRef<Float32Array | null>(null);\n  const preset = STATES[state] ?? STATES.thinking;\n  const active = status === \"active\" || !(status in STATUS_COLORS);\n  const tone = active ? preset : STATUS_COLORS[status as keyof typeof STATUS_COLORS];\n\n  React.useEffect(() => {\n    const levels = (levelsRef.current ??= new Float32Array(GRID * GRID));\n    const apply = () => {\n      cellsRef.current.forEach((cell, i) => {\n        if (cell) cell.style.opacity = String(levels[i] < 0.05 ? 0 : levels[i]);\n      });\n    };\n\n    const from = levels.slice();\n    const begin = performance.now();\n    let frame = 0;\n    const ease = (t: number) => 1 - (1 - Math.min(1, Math.max(0, t))) ** 3;\n\n    if (status === \"error\") {\n      const target = new Float32Array(GRID * GRID);\n      for (const i of ERROR_CELLS) target[i] = 1;\n      if (reduceMotion) {\n        levels.set(target);\n        apply();\n        return;\n      }\n      const settle = (now: number) => {\n        const eased = ease((now - begin) / 320);\n        for (let i = 0; i < levels.length; i++) {\n          levels[i] = from[i] + (target[i] - from[i]) * eased;\n        }\n        apply();\n        if (eased < 1) frame = requestAnimationFrame(settle);\n      };\n      frame = requestAnimationFrame(settle);\n      return () => cancelAnimationFrame(frame);\n    }\n\n    if (!active) {\n      if (reduceMotion) {\n        levels.fill(0);\n        apply();\n        return;\n      }\n      const settle = (now: number) => {\n        const eased = ease((now - begin) / 420);\n        for (let i = 0; i < levels.length; i++) levels[i] = from[i] * (1 - eased);\n        apply();\n        if (eased < 1) frame = requestAnimationFrame(settle);\n      };\n      frame = requestAnimationFrame(settle);\n      return () => cancelAnimationFrame(frame);\n    }\n\n    if (reduceMotion) {\n      paint(state, 0.9, levels);\n      apply();\n      return;\n    }\n\n    const next = new Float32Array(GRID * GRID);\n    const loop = (now: number) => {\n      paint(state, ((now - begin) / 1000) * Math.max(0.1, speed), next);\n      const mix = ease((now - begin) / 420);\n      for (let i = 0; i < levels.length; i++) {\n        levels[i] = from[i] + (next[i] - from[i]) * mix;\n      }\n      apply();\n      frame = requestAnimationFrame(loop);\n    };\n    frame = requestAnimationFrame(loop);\n    return () => cancelAnimationFrame(frame);\n  }, [state, status, active, speed, reduceMotion]);\n\n  const animateText = shimmer && !reduceMotion && active;\n  const loopColors = color === \"loop\" && !reduceMotion && active;\n  const fixed = color && color !== \"loop\" && status !== \"error\" ? color : undefined;\n  const label = children ?? preset.label;\n  const labelKey =\n    typeof label === \"string\" || typeof label === \"number\" ? String(label) : `node-${state}`;\n  const radius = pixel === \"circle\" ? 0.4 : 0.12;\n  const labelRef = React.useRef<HTMLSpanElement | null>(null);\n  const [labelBox, setLabelBox] = React.useState<{ width: number | null; settled: boolean }>({\n    width: null,\n    settled: false,\n  });\n  const labelWidth = labelBox.width;\n\n  React.useLayoutEffect(() => {\n    const node = labelRef.current;\n    if (!node) return;\n    const measure = () => {\n      const width = node.getBoundingClientRect().width;\n      setLabelBox((prev) => ({ width, settled: prev.width !== null }));\n    };\n    measure();\n    const observer = new ResizeObserver(measure);\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, [labelKey]);\n\n  const glide = labelBox.settled && !reduceMotion;\n\n  return (\n    <span\n      role=\"status\"\n      className={cn(\n        \"inline-flex items-center gap-[0.45em] align-middle font-medium\",\n        \"[--thinking-color:var(--thinking-light)] dark:[--thinking-color:var(--thinking-dark)]\",\n        loopColors &&\n          \"[animation:thinking-state-loop-light_7s_linear_infinite] dark:[animation-name:thinking-state-loop-dark]\",\n        className,\n      )}\n      style={\n        {\n          \"--thinking-light\": fixed ?? tone.light,\n          \"--thinking-dark\": fixed ?? tone.dark,\n          ...style,\n        } as React.CSSProperties\n      }\n      {...props}\n    >\n      {animateText && (\n        <style href=\"thinking-state-shimmer\" precedence=\"default\">\n          {SHIMMER_CSS}\n        </style>\n      )}\n      {loopColors && (\n        <style href=\"thinking-state-loop\" precedence=\"default\">\n          {LOOP_CSS}\n        </style>\n      )}\n      <svg aria-hidden viewBox={`0 0 ${GRID} ${GRID}`} className=\"size-[0.95em] shrink-0\">\n        {grid &&\n          Array.from({ length: GRID * GRID }, (_, i) =>\n            pixel === \"circle\" && !ROUND[i] ? null : (\n              <rect\n                key={i}\n                x={(i % GRID) + 0.1}\n                y={Math.floor(i / GRID) + 0.1}\n                width={0.8}\n                height={0.8}\n                rx={radius}\n                className=\"fill-muted-foreground opacity-20 dark:opacity-30\"\n              />\n            ),\n          )}\n        {Array.from({ length: GRID * GRID }, (_, i) =>\n          pixel === \"circle\" && !ROUND[i] ? null : (\n            <rect\n              key={`lit-${i}`}\n              ref={(node) => {\n                cellsRef.current[i] = node;\n              }}\n              x={(i % GRID) + 0.1}\n              y={Math.floor(i / GRID) + 0.1}\n              width={0.8}\n              height={0.8}\n              rx={radius}\n              className=\"fill-(--thinking-color)\"\n              opacity={0}\n            />\n          ),\n        )}\n      </svg>\n      <motion.span\n        className=\"inline-grid\"\n        initial={false}\n        animate={labelWidth === null ? undefined : { width: labelWidth }}\n        transition={{ duration: glide ? 0.3 : 0, ease: [0.4, 0, 0.2, 1] }}\n      >\n        <AnimatePresence initial={false}>\n          <motion.span\n            key={labelKey}\n            ref={(node: HTMLSpanElement | null) => {\n              if (node) labelRef.current = node;\n            }}\n            initial={\n              reduceMotion ? { opacity: 0 } : { opacity: 0, y: \"0.35em\", filter: \"blur(2px)\" }\n            }\n            animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n            exit={reduceMotion ? { opacity: 0 } : { opacity: 0, y: \"-0.35em\", filter: \"blur(2px)\" }}\n            transition={{ duration: reduceMotion ? 0 : 0.22, ease: [0.22, 1, 0.36, 1] }}\n            className={cn(\n              \"text-foreground w-max justify-self-start whitespace-nowrap [grid-area:1/1]\",\n              animateText &&\n                \"[animation:thinking-state-shimmer_3s_ease-in-out_infinite] bg-[linear-gradient(90deg,var(--foreground)_0%,var(--foreground)_30%,var(--muted-foreground)_50%,var(--foreground)_70%,var(--foreground)_100%)] bg-[length:300%_100%] bg-clip-text text-transparent\",\n            )}\n          >\n            {label}\n          </motion.span>\n        </AnimatePresence>\n      </motion.span>\n    </span>\n  );\n}\n\nThinkingState.displayName = \"ThinkingState\";\n\nexport function thinkingStateColors(\n  state: ThinkingStateKind,\n  status: ThinkingStateStatus = \"active\",\n): { light: string; dark: string } {\n  if (status === \"complete\" || status === \"error\") return STATUS_COLORS[status];\n  const { light, dark } = STATES[state] ?? STATES.thinking;\n  return { light, dark };\n}\n\nexport { ThinkingState };\n",
      "type": "registry:component"
    }
  ],
  "meta": {
    "addedAt": "2026-10-14"
  },
  "type": "registry:component"
}