{
  "$schema": "https://blode.co/ui/schema/registry-item.json",
  "name": "thinking-indicator",
  "title": "Thinking Indicator",
  "author": "Matthew Blode",
  "description": "An animated status indicator with a morphing glyph and cycling text for thinking states.",
  "dependencies": ["motion"],
  "files": [
    {
      "path": "ui/thinking-indicator.tsx",
      "content": "\"use client\";\n\nimport { AnimatePresence, motion } from \"motion/react\";\nimport type * as React from \"react\";\nimport { useEffect, useState } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\n// Three SVG paths the icon morphs between: a circle, a figure-eight, and the\n// same circle traced the other way. Cycling A → ∞ → B → ∞ → A reads as a calm,\n// continuous \"moonwalk\" without ever snapping.\nconst circleA =\n  \"M 12 8 C 14.21 8 16 9.79 16 12 C 16 14.21 14.21 16 12 16 C 9.79 16 8 14.21 8 12 C 8 9.79 9.79 8 12 8 Z\";\nconst infinity =\n  \"M 12 12 C 14 8.5 19 8.5 19 12 C 19 15.5 14 15.5 12 12 C 10 8.5 5 8.5 5 12 C 5 15.5 10 15.5 12 12 Z\";\nconst circleB =\n  \"M 12 16 C 14.21 16 16 14.21 16 12 C 16 9.79 14.21 8 12 8 C 9.79 8 8 9.79 8 12 C 8 14.21 9.79 16 12 16 Z\";\n\n// Self-contained shimmer: a clipped gradient swept across the text via motion,\n// so the component needs no global keyframe/CSS to render.\nconst SHIMMER_GRADIENT =\n  \"linear-gradient(90deg, var(--muted-foreground) 0%, var(--muted-foreground) 35%, var(--foreground) 50%, var(--muted-foreground) 65%, var(--muted-foreground) 100%)\";\n\nconst ShimmerText = ({\n  children,\n  className,\n}: {\n  children: React.ReactNode;\n  className?: string;\n}) => (\n  <motion.span\n    animate={{ backgroundPosition: [\"0% 0\", \"100% 0\"] }}\n    className={cn(\"bg-clip-text text-transparent leading-[1.22] py-1\", className)}\n    style={{ backgroundImage: SHIMMER_GRADIENT, backgroundSize: \"300% 100%\" }}\n    transition={{ duration: 1.5, ease: \"easeInOut\", repeat: Number.POSITIVE_INFINITY }}\n  >\n    {children}\n  </motion.span>\n);\n\nconst DEFAULT_WORDS = [\"Thinking\", \"Moonwalking\", \"Planning\", \"Refining\"];\n\ninterface ThinkingIndicatorProps extends React.ComponentProps<\"output\"> {\n  /** Labels cycled through while thinking. Defaults to a small built-in set. */\n  words?: string[];\n  /** Milliseconds each label is shown before swapping. Defaults to 4000. */\n  interval?: number;\n}\n\n// ─── ThinkingIndicator ──────────────────────────────────────────────────────\n// An animated status row: a morphing SVG glyph paired with shimmering, cycling\n// text. Drop it into a transcript while an assistant response is streaming.\nconst ThinkingIndicator = ({\n  className,\n  words = DEFAULT_WORDS,\n  interval = 4000,\n  ref,\n  ...props\n}: ThinkingIndicatorProps) => {\n  const [index, setIndex] = useState(0);\n\n  useEffect(() => {\n    const id = setInterval(() => {\n      setIndex((i) => (i + 1) % words.length);\n    }, interval);\n    return () => clearInterval(id);\n  }, [words.length, interval]);\n\n  // Reserve width for the longest label so the row never reflows mid-cycle.\n  let longest = words[0] ?? \"\";\n  for (const word of words) {\n    if (word.length > longest.length) {\n      longest = word;\n    }\n  }\n\n  return (\n    <output\n      className={cn(\"flex items-center gap-2 px-3 py-2\", className)}\n      data-slot=\"thinking-indicator\"\n      ref={ref}\n      {...props}\n    >\n      <motion.svg\n        aria-hidden\n        className=\"shrink-0 text-muted-foreground\"\n        fill=\"none\"\n        height={20}\n        stroke=\"currentColor\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        strokeWidth={1.5}\n        viewBox=\"0 0 24 24\"\n        width={20}\n      >\n        <motion.path\n          animate={{ d: [circleA, infinity, circleB, infinity, circleA] }}\n          transition={{\n            d: {\n              duration: 6,\n              ease: \"easeInOut\",\n              repeat: Number.POSITIVE_INFINITY,\n              times: [0, 0.25, 0.5, 0.75, 1],\n            },\n          }}\n        />\n      </motion.svg>\n      <span className=\"inline-grid overflow-hidden font-medium text-[13px]\">\n        <span aria-hidden className=\"invisible col-start-1 row-start-1\">\n          {longest}\n        </span>\n        <AnimatePresence initial={false} mode=\"popLayout\">\n          <motion.span\n            animate={{\n              opacity: 1,\n              transition: { duration: 0.24, ease: [0.4, 0, 0.2, 1] },\n              y: 0,\n            }}\n            className=\"col-start-1 row-start-1\"\n            exit={{\n              opacity: 0,\n              transition: { duration: 0.16, ease: [0.4, 0, 0.2, 1] },\n              y: \"-80%\",\n            }}\n            initial={{ opacity: 0, y: \"80%\" }}\n            key={words[index]}\n          >\n            <ShimmerText>{words[index]}</ShimmerText>\n          </motion.span>\n        </AnimatePresence>\n      </span>\n    </output>\n  );\n};\n\nexport { ThinkingIndicator };\nexport type { ThinkingIndicatorProps };\n",
      "type": "registry:ui",
      "target": ""
    }
  ],
  "type": "registry:ui"
}
