{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "thinking-indicator",
  "type": "registry:ui",
  "title": "Thinking Indicator",
  "description": "Compact AI “thinking” indicators — dots, pulse, orbit, wave, or cycling status text — inside a polite live region.",
  "dependencies": [],
  "registryDependencies": [
    "https://kuro.abstergo.fyi/r/kura-theme.json",
    "https://kuro.abstergo.fyi/r/shimmer-text.json",
    "utils"
  ],
  "files": [
    {
      "path": "registry/kura/thinking-indicator/index.ts",
      "type": "registry:ui",
      "target": "components/kura/thinking-indicator/index.ts",
      "content": "export { ThinkingIndicator, type ThinkingIndicatorProps } from \"./thinking-indicator\";\n"
    },
    {
      "path": "registry/kura/thinking-indicator/thinking-indicator.tsx",
      "type": "registry:ui",
      "target": "components/kura/thinking-indicator/thinking-indicator.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { ShimmerText } from \"@/components/kura/shimmer-text\";\n\nexport type ThinkingIndicatorVariant = \"dots\" | \"pulse\" | \"orbit\" | \"wave\" | \"text\";\nexport type ThinkingIndicatorSize = \"sm\" | \"md\" | \"lg\";\n\nconst CSS = `\n@keyframes kura-ti-dot {\n  0%, 64%, 100% { transform: translateY(0); opacity: 0.35; animation-timing-function: cubic-bezier(0.34, 1.5, 0.64, 1); }\n  32% { transform: translateY(-70%); opacity: 1; animation-timing-function: cubic-bezier(0.55, 0, 0.45, 1); }\n}\n@keyframes kura-ti-ring { from { transform: scale(1); opacity: 0.55; } to { transform: scale(2.8); opacity: 0; } }\n@keyframes kura-ti-breathe { 0%, 100% { transform: scale(0.82); } 50% { transform: scale(1); } }\n@keyframes kura-ti-orbit { to { transform: rotate(360deg); } }\n@keyframes kura-ti-bar { 0%, 100% { transform: scaleY(0.32); opacity: 0.5; } 50% { transform: scaleY(1); opacity: 1; } }\n`;\n\nconst SIZE: Record<ThinkingIndicatorSize, string> = {\n  sm: \"[--ti-dot:4px] [--ti-gap:3px]\",\n  md: \"[--ti-dot:6px] [--ti-gap:4px]\",\n  lg: \"[--ti-dot:8px] [--ti-gap:5px]\",\n};\n\nconst TEXT_SIZE: Record<ThinkingIndicatorSize, string> = {\n  sm: \"text-xs\",\n  md: \"text-sm\",\n  lg: \"text-[15px]\",\n};\n\nexport interface ThinkingIndicatorProps extends Omit<React.ComponentProps<\"span\">, \"children\"> {\n  /** Visual style. `text` cycles through `messages`. Default \"dots\". */\n  variant?: ThinkingIndicatorVariant;\n  /** Overall scale. Default \"md\". */\n  size?: ThinkingIndicatorSize;\n  /** Accessible status text announced to screen readers. Default \"Thinking\". */\n  label?: string;\n  /**\n   * Status lines for the `text` variant, e.g. [\"Searching…\", \"Reading 4 files…\"]. Several lines\n   * cycle on a timer with a cross-fade; a single line is shown (and announced) as is.\n   */\n  messages?: readonly string[];\n  /** Milliseconds each message stays up when cycling. Default 2400. */\n  interval?: number;\n}\n\nconst motion = \"motion-reduce:animate-none\";\n\nfunction Dots() {\n  return (\n    <span data-slot=\"thinking-indicator-shape\" className=\"inline-flex items-center gap-(--ti-gap)\">\n      {[0, 1, 2].map((i) => (\n        <span\n          key={i}\n          className={cn(\n            \"size-(--ti-dot) rounded-full bg-current [animation:kura-ti-dot_1.25s_infinite]\",\n            motion,\n            i === 0 && \"motion-reduce:opacity-40\",\n            i === 1 && \"motion-reduce:opacity-70\",\n          )}\n          style={{ animationDelay: `${i * 0.14}s` }}\n        />\n      ))}\n    </span>\n  );\n}\n\nfunction Pulse() {\n  return (\n    <span\n      data-slot=\"thinking-indicator-shape\"\n      className=\"relative inline-flex size-[calc(var(--ti-dot)*2.6)] items-center justify-center\"\n    >\n      <span\n        className={cn(\n          \"absolute size-(--ti-dot) rounded-full bg-current [animation:kura-ti-ring_1.6s_cubic-bezier(0.23,1,0.32,1)_infinite]\",\n          motion,\n          \"motion-reduce:opacity-0\",\n        )}\n      />\n      <span\n        className={cn(\n          \"size-[calc(var(--ti-dot)*1.25)] rounded-full bg-current [animation:kura-ti-breathe_1.6s_cubic-bezier(0.77,0,0.175,1)_infinite]\",\n          motion,\n        )}\n      />\n    </span>\n  );\n}\n\nfunction Orbit() {\n  return (\n    <span\n      data-slot=\"thinking-indicator-shape\"\n      className=\"relative inline-flex size-[calc(var(--ti-dot)*3.2)] items-center justify-center\"\n    >\n      <span className=\"absolute inset-[calc(var(--ti-dot)*0.5)] rounded-full border border-current opacity-20\" />\n      {[0, 1].map((i) => (\n        <span\n          key={i}\n          className={cn(\n            \"absolute inset-0 [animation:kura-ti-orbit_1.4s_cubic-bezier(0.45,0.05,0.55,0.95)_infinite]\",\n            motion,\n          )}\n          style={{\n            animationDelay: `${-i * 0.18}s`,\n            transform: `rotate(${i * 140}deg)`,\n          }}\n        >\n          <span\n            className=\"absolute top-0 left-1/2 size-(--ti-dot) -translate-x-1/2 rounded-full bg-current\"\n            style={{ opacity: i === 0 ? 1 : 0.45, scale: i === 0 ? \"1\" : \"0.75\" }}\n          />\n        </span>\n      ))}\n    </span>\n  );\n}\n\nfunction Wave() {\n  const still = [0.45, 0.8, 1, 0.6];\n  return (\n    <span\n      data-slot=\"thinking-indicator-shape\"\n      className=\"inline-flex h-[calc(var(--ti-dot)*2.6)] items-center gap-[calc(var(--ti-gap)*0.6)]\"\n    >\n      {still.map((s, i) => (\n        <span\n          key={i}\n          className={cn(\n            \"h-full w-[calc(var(--ti-dot)*0.55)] rounded-full bg-current [animation:kura-ti-bar_1s_cubic-bezier(0.77,0,0.175,1)_infinite]\",\n            motion,\n            \"motion-reduce:scale-y-(--ti-still)\",\n          )}\n          style={{ animationDelay: `${i * 0.12}s`, [\"--ti-still\" as string]: s }}\n        />\n      ))}\n    </span>\n  );\n}\n\n/**\n * Compact AI \"thinking\" indicators — dots, pulse, orbit, wave, or cycling status text — wrapped in\n * a polite live region. Shapes use `currentColor` (jade by default); restyle with a text-colour\n * class. CSS-only animation; everything settles to a still glyph under reduced motion.\n */\nexport function ThinkingIndicator({\n  variant = \"dots\",\n  size = \"md\",\n  label = \"Thinking\",\n  messages,\n  interval = 2400,\n  className,\n  ...props\n}: ThinkingIndicatorProps) {\n  const lines = messages?.length ? messages : [`${label}…`];\n  const cycling = variant === \"text\" && lines.length > 1;\n  const [index, setIndex] = React.useState(0);\n  const key = lines.join(\"\\n\");\n\n  // biome-ignore lint/correctness/useExhaustiveDependencies: restart when the message list changes.\n  React.useEffect(() => {\n    setIndex(0);\n    if (!cycling) return;\n    const id = setInterval(() => setIndex((i) => (i + 1) % lines.length), Math.max(600, interval));\n    return () => clearInterval(id);\n  }, [cycling, key, interval]);\n\n  const shape =\n    variant === \"dots\" ? (\n      <Dots />\n    ) : variant === \"pulse\" ? (\n      <Pulse />\n    ) : variant === \"orbit\" ? (\n      <Orbit />\n    ) : variant === \"wave\" ? (\n      <Wave />\n    ) : null;\n\n  return (\n    <span\n      role=\"status\"\n      aria-live=\"polite\"\n      data-slot=\"thinking-indicator\"\n      data-variant={variant}\n      data-size={size}\n      className={cn(\n        \"inline-flex items-center gap-2 align-middle text-kura-accent\",\n        SIZE[size],\n        className,\n      )}\n      {...props}\n    >\n      <style href=\"kura-thinking-indicator\" precedence=\"kura\">\n        {CSS}\n      </style>\n      {shape && (\n        <>\n          <span aria-hidden className=\"contents\">\n            {shape}\n          </span>\n          <span className=\"sr-only\">{label}</span>\n        </>\n      )}\n      {variant === \"text\" && (\n        <>\n          <span aria-hidden className=\"contents\">\n            <Pulse />\n          </span>\n          {cycling && <span className=\"sr-only\">{label}</span>}\n          <span\n            data-slot=\"thinking-indicator-text\"\n            aria-hidden={cycling || undefined}\n            className={cn(\"grid text-kura-fg-muted\", TEXT_SIZE[size])}\n          >\n            {lines.map((line, i) => (\n              <span\n                key={`${i}-${line}`}\n                data-active={i === index}\n                className={cn(\n                  \"col-start-1 row-start-1 whitespace-nowrap transition-[opacity,filter,translate] duration-(--kura-duration-slow) ease-kura-out motion-reduce:translate-y-0 motion-reduce:blur-none\",\n                  i === index ? \"opacity-100\" : \"translate-y-0.5 opacity-0 blur-[2px]\",\n                )}\n              >\n                {i === index ? <ShimmerText>{line}</ShimmerText> : line}\n              </span>\n            ))}\n          </span>\n        </>\n      )}\n    </span>\n  );\n}\n"
    }
  ],
  "categories": [
    "feedback",
    "loading",
    "thinking",
    "typing",
    "dots"
  ],
  "docs": "Docs: https://kuro.abstergo.fyi/components/thinking-indicator · Markdown for agents: https://kuro.abstergo.fyi/components/thinking-indicator.md",
  "meta": {
    "kura": {
      "status": "beta",
      "whenToUse": [
        "The assistant has received a prompt but not produced its first token yet.",
        "A pending assistant message bubble or a placeholder row in a chat list.",
        "Cycling short status lines while an agent works: “Searching…”, “Reading 4 files…” (variant text).",
        "A tiny inline signal next to a label, tab or button that work is in progress."
      ],
      "whenNotToUse": [
        "Shimmering an existing label in place — use shimmer-text.",
        "Network or file loading that is not model thinking — use spinner.",
        "A whole container being worked on — use border-beam.",
        "A voice agent's state — use agent-orb.",
        "Streaming the answer itself — use streaming-text."
      ]
    }
  }
}
