{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "thinking-indicator", "title": "Thinking Indicator", "description": "Animated loading indicator with morphing SVG path (circle to infinity loop) and shimmer text rotation. Cycles through customizable status words.", "dependencies": [ "framer-motion" ], "registryDependencies": [ "https://zeron-ui.vercel.app/r/surfaces.json", "utils" ], "files": [ { "path": "src/components/ui/thinking-indicator.tsx", "content": "\"use client\";\n\nimport { forwardRef, useState, useEffect, type HTMLAttributes } from \"react\";\nimport { motion, AnimatePresence, useReducedMotion } from \"framer-motion\";\nimport { cn } from \"@/lib/utils\";\nimport { spring } from \"@/lib/springs\";\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\";\n\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\";\n\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\nconst words = [\"Thinking\", \"Moonwalking\", \"Planning\", \"Refining\"];\n\ninterface ThinkingIndicatorProps extends HTMLAttributes {\n /** Show the morphing circle⇄infinity glyph before the label. Set to `false`\n * for a text-only indicator (e.g. inline before a streamed reply). */\n showIcon?: boolean;\n}\n\nconst ThinkingIndicator = forwardRef(\n ({ className, showIcon = true, ...props }, ref) => {\n const [index, setIndex] = useState(0);\n // Reduced motion drops the infinite glyph morph and the word cycling — a\n // static glyph and label carry the same meaning without the movement.\n const reduceMotion = useReducedMotion() ?? false;\n\n useEffect(() => {\n if (reduceMotion) return;\n const interval = setInterval(() => {\n setIndex((i) => (i + 1) % words.length);\n }, 4000);\n return () => clearInterval(interval);\n }, [reduceMotion]);\n\n return (\n \n {/* Static announcement — the cycling word display below is aria-hidden\n so screen readers hear one \"Thinking…\" instead of a re-announcement\n every 4 seconds. */}\n Thinking…\n {showIcon && (\n \n {reduceMotion ? (\n \n ) : (\n \n )}\n \n )}\n \n \n {words.reduce((a, b) => (a.length >= b.length ? a : b))}\n \n {reduceMotion ? (\n \n {words[0]}\n \n ) : (\n \n \n {words[index]}\n \n \n )}\n \n \n );\n});\n\nThinkingIndicator.displayName = \"ThinkingIndicator\";\n\nexport { ThinkingIndicator };\nexport type { ThinkingIndicatorProps };\nexport default ThinkingIndicator;\n", "type": "registry:ui", "target": "components/ui/thinking-indicator.tsx" } ], "css": { "@keyframes shimmer": { "0%": { "background-position": "0% 0" }, "100%": { "background-position": "100% 0" } }, ".shimmer-text": { "color": "transparent", "background": "linear-gradient(90deg, #a3a3a3 0%, #a3a3a3 35%, #525252 50%, #a3a3a3 65%, #a3a3a3 100%)", "background-size": "300% 100%", "background-clip": "text", "-webkit-background-clip": "text", "animation": "shimmer 1.5s ease-in-out infinite" } }, "type": "registry:ui" }