{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "thinking-indicator",
  "title": "Thinking Indicator",
  "description": "A status line for an assistant that is working, with dots, bars, wave or shimmer animation, rotating labels and an optional timer.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/tealmac/thinking-indicator.tsx",
      "content": "// Adapted from Ballmac UI (https://github.com/vamsiy78/ballmac-ui), MIT License, Copyright (c) 2026 Ballmac.\n\"use client\"\n\nimport * as React from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\ntype ThinkingIndicatorProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  /** Animation shown beside the text. \"dots\" bounces three dots, \"bars\" is an equalizer, \"wave\" is a soft sine ribbon and \"shimmer\" sweeps light across the text only. */\n  variant?: \"dots\" | \"bars\" | \"wave\" | \"shimmer\"\n  /** One label, or several that take turns, such as \"Reading the files\" then \"Drafting a reply\". */\n  label?: string | string[]\n  /** Milliseconds each label stays before the next one. */\n  interval?: number\n  /** Shows a running timer such as \"12s\" or \"1:05\". */\n  showTimer?: boolean\n  /** Elapsed seconds. When set the timer follows this value instead of counting by itself. */\n  elapsed?: number\n  /** What screen readers hear when thinking starts. The cycling labels are visual only, so a long task does not chatter. */\n  statusLabel?: string\n  /** Text size of the label. */\n  size?: \"sm\" | \"default\"\n}\n\nconst noopSubscribe = () => () => {}\n\n/** useReducedMotion that returns false until hydration, so server and client markup match. */\nfunction useReducedMotionSafe() {\n  const reduce = useReducedMotion()\n  const hydrated = React.useSyncExternalStore(\n    noopSubscribe,\n    () => true,\n    () => false\n  )\n  return hydrated && !!reduce\n}\n\nfunction formatElapsed(total: number) {\n  const s = Math.max(0, Math.floor(total))\n  if (s < 60) return `${s}s`\n  return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, \"0\")}`\n}\n\nfunction Dots({ reduce }: { reduce: boolean }) {\n  return (\n    <span aria-hidden=\"true\" className=\"flex h-4 items-center gap-1\">\n      {[0, 1, 2].map((i) => (\n        <motion.span\n          key={i}\n          className=\"bg-foreground/70 size-1.5 rounded-full\"\n          animate={\n            reduce\n              ? { opacity: 0.6 }\n              : { y: [0, -4, 0], opacity: [0.35, 1, 0.35] }\n          }\n          transition={\n            reduce\n              ? undefined\n              : {\n                  duration: 0.9,\n                  repeat: Infinity,\n                  delay: i * 0.15,\n                  ease: \"easeInOut\",\n                }\n          }\n        />\n      ))}\n    </span>\n  )\n}\n\nfunction Bars({ reduce }: { reduce: boolean }) {\n  const heights = [0.45, 0.85, 0.6, 1, 0.5]\n  return (\n    <span aria-hidden=\"true\" className=\"flex h-4 items-center gap-[3px]\">\n      {heights.map((h, i) => (\n        <motion.span\n          key={i}\n          className=\"bg-foreground/70 block w-[3px] origin-center rounded-full\"\n          style={{ height: \"100%\" }}\n          initial={false}\n          animate={reduce ? { scaleY: h } : { scaleY: [h * 0.45, 1, h * 0.45] }}\n          transition={\n            reduce\n              ? undefined\n              : {\n                  duration: 1.1,\n                  repeat: Infinity,\n                  delay: i * 0.12,\n                  ease: \"easeInOut\",\n                }\n          }\n        />\n      ))}\n    </span>\n  )\n}\n\nfunction Wave({ reduce }: { reduce: boolean }) {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      viewBox=\"0 0 48 16\"\n      className=\"text-foreground/70 h-4 w-12 overflow-hidden\"\n    >\n      {[0, 1, 2].map((i) => (\n        <g key={i} transform={`translate(0 ${(i - 1) * 1.5})`}>\n          <motion.path\n            d=\"M0 8 Q 6 1, 12 8 T 24 8 T 36 8 T 48 8 T 60 8 T 72 8\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeWidth={1.5}\n            strokeLinecap=\"round\"\n            opacity={1 - i * 0.32}\n            animate={reduce ? undefined : { x: [0, -24] }}\n            transition={\n              reduce\n                ? undefined\n                : { duration: 1.6 + i * 0.4, repeat: Infinity, ease: \"linear\" }\n            }\n          />\n        </g>\n      ))}\n    </svg>\n  )\n}\n\nfunction ThinkingIndicator({\n  variant = \"dots\",\n  label,\n  interval = 2600,\n  showTimer = false,\n  elapsed,\n  statusLabel,\n  size = \"default\",\n  className,\n  ...props\n}: ThinkingIndicatorProps) {\n  label ??= \"Thinking\"\n  statusLabel ??= \"Assistant is thinking\"\n  const reduce = useReducedMotionSafe()\n  const labels = React.useMemo(\n    () => (Array.isArray(label) ? label : [label]),\n    [label]\n  )\n  const [index, setIndex] = React.useState(0)\n  const [seconds, setSeconds] = React.useState(0)\n  const controlled = elapsed !== undefined\n\n  React.useEffect(() => {\n    if (labels.length < 2) return\n    const id = setInterval(\n      () => setIndex((i) => (i + 1) % labels.length),\n      interval\n    )\n    return () => clearInterval(id)\n  }, [labels.length, interval])\n\n  React.useEffect(() => {\n    if (!showTimer || controlled) return\n    const id = setInterval(() => setSeconds((s) => s + 1), 1000)\n    return () => clearInterval(id)\n  }, [showTimer, controlled])\n\n  const current = labels[Math.min(index, labels.length - 1)] ?? \"\"\n  const shown = controlled ? elapsed : seconds\n  const textClass = size === \"sm\" ? \"text-xs\" : \"text-sm\"\n\n  const text = (\n    <span\n      className={cn(\"relative inline-grid overflow-hidden\", textClass)}\n      aria-hidden=\"true\"\n    >\n      <AnimatePresence mode=\"wait\" initial={false}>\n        <motion.span\n          key={current}\n          className={cn(\n            \"block whitespace-nowrap\",\n            variant === \"shimmer\" && !reduce\n              ? \"bg-clip-text text-transparent\"\n              : \"text-muted-foreground\"\n          )}\n          style={\n            variant === \"shimmer\" && !reduce\n              ? {\n                  backgroundImage:\n                    \"linear-gradient(90deg, var(--muted-foreground) 35%, var(--foreground) 50%, var(--muted-foreground) 65%)\",\n                  backgroundSize: \"250% 100%\",\n                }\n              : undefined\n          }\n          initial={reduce ? { opacity: 0 } : { opacity: 0, y: 8 }}\n          animate={\n            variant === \"shimmer\" && !reduce\n              ? { opacity: 1, y: 0, backgroundPosition: [\"100% 0%\", \"0% 0%\"] }\n              : { opacity: 1, y: 0 }\n          }\n          exit={reduce ? { opacity: 0 } : { opacity: 0, y: -8 }}\n          transition={{\n            duration: 0.22,\n            backgroundPosition:\n              variant === \"shimmer\"\n                ? { duration: 1.8, repeat: Infinity, ease: \"linear\" }\n                : undefined,\n          }}\n        >\n          {current}\n        </motion.span>\n      </AnimatePresence>\n    </span>\n  )\n\n  return (\n    <div\n      data-slot=\"thinking-indicator\"\n      data-variant={variant}\n      role=\"status\"\n      className={cn(\"inline-flex items-center gap-2.5\", className)}\n      {...props}\n    >\n      {variant === \"dots\" && <Dots reduce={reduce} />}\n      {variant === \"bars\" && <Bars reduce={reduce} />}\n      {variant === \"wave\" && <Wave reduce={reduce} />}\n      {text}\n      {showTimer && (\n        <span\n          aria-hidden=\"true\"\n          className=\"text-muted-foreground before:text-border font-mono text-[11px] tabular-nums before:me-2 before:content-['·']\"\n        >\n          {formatElapsed(shown)}\n        </span>\n      )}\n      <span className=\"sr-only\">{statusLabel}</span>\n    </div>\n  )\n}\n\nexport { ThinkingIndicator, formatElapsed, type ThinkingIndicatorProps }\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}