{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "thinking-indicator",
  "title": "Thinking Indicator",
  "description": "Three breathing dots and a live label that tell the user the model is working, and on what.",
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/miseui/ui/thinking-indicator.tsx",
      "content": "import type * as React from \"react\"\nimport { Check, TriangleAlert } from \"lucide-react\"\nimport { motion, useReducedMotion } from \"motion/react\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\ntype ThinkingState = \"thinking\" | \"searching\" | \"writing\" | \"done\" | \"error\"\ntype ThinkingSize = \"sm\" | \"md\" | \"lg\"\n\nconst DEFAULT_LABELS: Record<ThinkingState, string> = {\n  thinking: \"Thinking…\",\n  searching: \"Searching…\",\n  writing: \"Writing…\",\n  done: \"Done\",\n  error: \"Something went wrong\",\n}\n\nconst ACTIVE_STATES: readonly ThinkingState[] = [\"thinking\", \"searching\", \"writing\"]\nconst DOT_COUNT = 3\n/** One full breath per dot. Under a second so the wait never feels frozen. */\nconst PULSE_SECONDS = 0.9\nconst DOT_STAGGER_SECONDS = 0.15\nconst REDUCED_MOTION_BASE_OPACITY = 0.4\nconst REDUCED_MOTION_OPACITY_STEP = 0.25\n\nconst SIZE_CLASSES: Record<ThinkingSize, { dot: string; text: string; icon: string; gap: string }> = {\n  sm: { dot: \"size-1.5\", text: \"text-xs\", icon: \"size-3.5\", gap: \"gap-1\" },\n  md: { dot: \"size-2\", text: \"text-sm\", icon: \"size-4\", gap: \"gap-1.5\" },\n  lg: { dot: \"size-2.5\", text: \"text-base\", icon: \"size-5\", gap: \"gap-2\" },\n}\n\ninterface ThinkingIndicatorProps extends React.ComponentProps<\"div\"> {\n  /** What the model is doing right now. Active states show breathing dots. */\n  state?: ThinkingState\n  /** Name the work (\"Reading 3 files…\"). Falls back to a label for the state. */\n  label?: string\n  size?: ThinkingSize\n}\n\nfunction ThinkingIndicator({\n  state = \"thinking\",\n  label,\n  size = \"md\",\n  className,\n  ...props\n}: ThinkingIndicatorProps) {\n  const isActive = ACTIVE_STATES.includes(state)\n  const sizeClass = SIZE_CLASSES[size]\n\n  return (\n    <div\n      role=\"status\"\n      aria-live=\"polite\"\n      data-slot=\"thinking-indicator\"\n      data-state={state}\n      className={cn(\n        \"inline-flex items-center gap-2.5 text-muted-foreground\",\n        sizeClass.text,\n        state === \"done\" && \"text-foreground\",\n        state === \"error\" && \"text-destructive\",\n        className,\n      )}\n      {...props}\n    >\n      {isActive ? (\n        <BreathingDots dotClassName={sizeClass.dot} gapClassName={sizeClass.gap} />\n      ) : (\n        <StateIcon state={state} className={sizeClass.icon} />\n      )}\n      <span>{label ?? DEFAULT_LABELS[state]}</span>\n    </div>\n  )\n}\n\nfunction BreathingDots({ dotClassName, gapClassName }: { dotClassName: string; gapClassName: string }) {\n  const reduceMotion = useReducedMotion()\n\n  return (\n    <span className={cn(\"flex items-center\", gapClassName)} aria-hidden=\"true\">\n      {Array.from({ length: DOT_COUNT }, (_, index) => (\n        <motion.span\n          key={index}\n          data-testid=\"thinking-dot\"\n          className={cn(\"rounded-full bg-mise\", dotClassName)}\n          initial={false}\n          animate={\n            reduceMotion\n              ? { opacity: REDUCED_MOTION_BASE_OPACITY + index * REDUCED_MOTION_OPACITY_STEP }\n              : { scale: [1, 1.5, 1], opacity: [0.45, 1, 0.45] }\n          }\n          transition={\n            reduceMotion\n              ? { duration: 0 }\n              : {\n                  duration: PULSE_SECONDS,\n                  repeat: Infinity,\n                  ease: \"easeInOut\",\n                  delay: index * DOT_STAGGER_SECONDS,\n                }\n          }\n        />\n      ))}\n    </span>\n  )\n}\n\nfunction StateIcon({ state, className }: { state: ThinkingState; className: string }) {\n  if (state === \"done\") return <Check className={className} aria-hidden=\"true\" />\n  if (state === \"error\") return <TriangleAlert className={className} aria-hidden=\"true\" />\n  return null\n}\n\nexport { ThinkingIndicator, type ThinkingIndicatorProps, type ThinkingState }\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "ai"
  ],
  "type": "registry:ui"
}