{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"thinking-indicator","type":"registry:ui","title":"ThinkingIndicator","description":"Live agent status line with a shimmering activity label and an elapsed timer, as a bare inline row or a bordered status pill","author":"fancy-ui <https://fancy-ui.rama.app>","categories":["ai-agents"],"registryDependencies":["https://fancy-ui.rama.app/r/internals.json","https://fancy-ui.rama.app/r/internals-dom.json","https://fancy-ui.rama.app/r/utils.json"],"files":[{"path":"react/src/components/thinking-indicator/ThinkingIndicator.tsx","content":"\"use client\";\n\nimport { forwardRef, useEffect, type ReactNode } from \"react\";\nimport { cn } from \"../../utils\";\nimport { formatElapsed, useElapsed } from \"../../internals/use-elapsed\";\nimport \"./thinking-indicator.css\";\n\n/**\n * Props for ThinkingIndicator\n */\nexport interface ThinkingIndicatorProps {\n\t/** What the agent is doing right now, e.g. \"Reading files\" */\n\tstatus: string;\n\t/** Whether the activity is still in flight: drives the shimmer and the live timer */\n\trunning?: boolean;\n\t/** `\"inline\"` is a bare text row; `\"pill\"` is a bordered chip with a leading pulse dot */\n\tvariant?: \"inline\" | \"pill\";\n\t/** Epoch ms the activity started; the internal stopwatch ticks from it */\n\tsince?: number;\n\t/** Externally-driven elapsed time in ms; overrides the internal stopwatch */\n\telapsedMs?: number;\n\t/** Show the elapsed duration alongside the status */\n\tshowElapsed?: boolean;\n\t/** Rendered instead of the status label once `running` is false */\n\tdone?: ReactNode;\n\t/** Additional CSS classes */\n\tclassName?: string;\n}\n\n/**\n * ThinkingIndicator\n */\nexport const ThinkingIndicator = forwardRef<HTMLDivElement, ThinkingIndicatorProps>(\n\tfunction ThinkingIndicator(\n\t\t{ status, running = true, variant = \"inline\", since, elapsedMs, showElapsed = true, done, className },\n\t\tref\n\t) {\n\t\t// Seeded at zero rather than from `since`: reading the wall clock during\n\t\t// render would happen once on the server and again at hydration, and the\n\t\t// two can land on different seconds, leaving the `<time>` text and its\n\t\t// `datetime` disagreeing with the markup being hydrated. The effect below\n\t\t// reads the real elapsed time immediately after mounting — client-only —\n\t\t// so a row that mounts already finished stays at 0s, which is what the\n\t\t// README documents for that case.\n\t\tconst elapsed = useElapsed();\n\n\t\t// `start` returns its own stop function, which doubles as the effect\n\t\t// cleanup: flipping `running` off, swapping in an external `elapsedMs`, or\n\t\t// changing `since` all tear the interval down before the next run starts a\n\t\t// new one.\n\t\tconst { start } = elapsed;\n\t\tuseEffect(() => {\n\t\t\tif (running && since !== undefined && elapsedMs === undefined) return start(since);\n\t\t}, [running, since, elapsedMs, start]);\n\n\t\tconst hasElapsed = elapsedMs !== undefined || since !== undefined;\n\t\tconst effectiveMs = elapsedMs ?? elapsed.ms;\n\t\tconst elapsedText = formatElapsed(effectiveMs);\n\t\t// ISO 8601 duration, so the value is machine-readable and not just decorative.\n\t\tconst elapsedDateTime = `PT${Math.max(0, Math.floor(effectiveMs / 1000))}S`;\n\t\tconst showDone = !running && done !== undefined;\n\n\t\treturn (\n\t\t\t<div\n\t\t\t\tref={ref}\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"ft-thinking inline-flex items-center gap-2 text-sm\",\n\t\t\t\t\tvariant === \"pill\" && \"ft-thinking-pill bg-background/60 rounded-full border px-3 py-1\",\n\t\t\t\t\tclassName\n\t\t\t\t)}\n\t\t\t\trole=\"status\"\n\t\t\t\taria-live=\"polite\"\n\t\t\t>\n\t\t\t\t{variant === \"pill\" && (\n\t\t\t\t\t<span\n\t\t\t\t\t\tclassName={cn(\"ft-thinking-dot\", running && \"ft-thinking-dot-live\")}\n\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t></span>\n\t\t\t\t)}\n\n\t\t\t\t{showDone ? (\n\t\t\t\t\t<span className=\"ft-thinking-done text-muted-foreground\">{done}</span>\n\t\t\t\t) : (\n\t\t\t\t\t<span className={cn(\"ft-thinking-label text-foreground\", running && \"ft-thinking-shimmer\")}>\n\t\t\t\t\t\t{status}\n\t\t\t\t\t</span>\n\t\t\t\t)}\n\n\t\t\t\t{showElapsed && hasElapsed && (\n\t\t\t\t\t/*\n\t\t\t\t\t\tWhile the timer ticks it is hidden from assistive tech: inside a polite\n\t\t\t\t\t\tlive region a visible second counter would otherwise be announced once\n\t\t\t\t\t\tper second. It becomes readable again the moment the activity stops.\n\t\t\t\t\t*/\n\t\t\t\t\t<time\n\t\t\t\t\t\tclassName=\"ft-thinking-elapsed text-muted-foreground tabular-nums\"\n\t\t\t\t\t\tdateTime={elapsedDateTime}\n\t\t\t\t\t\taria-hidden={running ? \"true\" : undefined}\n\t\t\t\t\t>\n\t\t\t\t\t\t{elapsedText}\n\t\t\t\t\t</time>\n\t\t\t\t)}\n\t\t\t</div>\n\t\t);\n\t}\n);\n","type":"registry:ui","target":"@ui/fancy/components/thinking-indicator/ThinkingIndicator.tsx"},{"path":"react/src/components/thinking-indicator/index.ts","content":"\"use client\";\n\nexport { ThinkingIndicator } from \"./ThinkingIndicator\";\nexport type { ThinkingIndicatorProps } from \"./ThinkingIndicator\";\n","type":"registry:ui","target":"@ui/fancy/components/thinking-indicator/index.ts"},{"path":"react/src/components/thinking-indicator/thinking-indicator.css","content":".ft-thinking {\n\t/* Everything is expressed relative to currentColor, so one set of values\n\t   reads correctly in both light and dark themes. */\n\t--ft-thinking-shimmer-base: color-mix(in oklab, currentColor 45%, transparent);\n\t--ft-thinking-shimmer-highlight: currentColor;\n\t--ft-thinking-shimmer-duration: 2.2s;\n\t--ft-thinking-pulse-duration: 1.6s;\n}\n\n.ft-thinking .ft-thinking-dot {\n\tposition: relative;\n\tdisplay: inline-block;\n\tflex: none;\n\twidth: 0.4375rem;\n\theight: 0.4375rem;\n\tborder-radius: 9999px;\n\tbackground: currentColor;\n}\n\n.ft-thinking .ft-thinking-dot::after {\n\tcontent: \"\";\n\tposition: absolute;\n\tinset: 0;\n\tborder-radius: inherit;\n\tbackground: currentColor;\n\topacity: 0;\n}\n\n/*\n * Both animations live behind `no-preference`, so reduced motion is not a\n * degraded variant to maintain: the label keeps its normal colour and the dot\n * is simply a static dot.\n */\n@media (prefers-reduced-motion: no-preference) {\n\t.ft-thinking .ft-thinking-shimmer {\n\t\tbackground-image: linear-gradient(\n\t\t\t100deg,\n\t\t\tvar(--ft-thinking-shimmer-base) 0%,\n\t\t\tvar(--ft-thinking-shimmer-base) 35%,\n\t\t\tvar(--ft-thinking-shimmer-highlight) 50%,\n\t\t\tvar(--ft-thinking-shimmer-base) 65%,\n\t\t\tvar(--ft-thinking-shimmer-base) 100%\n\t\t);\n\t\tbackground-size: 250% 100%;\n\t\t/* Default `repeat` keeps the gradient tiling over the glyphs for every\n\t\t   background-position the sweep visits; `no-repeat` would blank the label\n\t\t   whenever the image travels outside the box. */\n\t\t-webkit-background-clip: text;\n\t\tbackground-clip: text;\n\t\t/* Keep `color` intact: the gradient stops are built from currentColor, and\n\t\t   custom properties substitute at the use site — `color: transparent` here\n\t\t   would zero every stop and render the label invisible. Only the fill is\n\t\t   made transparent so the clipped gradient shows through. */\n\t\t-webkit-text-fill-color: transparent;\n\t\tanimation: ft-thinking-sweep var(--ft-thinking-shimmer-duration) linear infinite;\n\t}\n\n\t.ft-thinking .ft-thinking-dot-live::after {\n\t\tanimation: ft-thinking-pulse var(--ft-thinking-pulse-duration) cubic-bezier(0, 0, 0.2, 1)\n\t\t\tinfinite;\n\t}\n}\n\n@keyframes ft-thinking-sweep {\n\tfrom {\n\t\tbackground-position: 200% 0;\n\t}\n\tto {\n\t\tbackground-position: -100% 0;\n\t}\n}\n\n@keyframes ft-thinking-pulse {\n\t0% {\n\t\ttransform: scale(1);\n\t\topacity: 0.55;\n\t}\n\t75%,\n\t100% {\n\t\ttransform: scale(2.6);\n\t\topacity: 0;\n\t}\n}\n","type":"registry:ui","target":"@ui/fancy/components/thinking-indicator/thinking-indicator.css"}],"meta":{"license":"MIT","package":"fancy-ui-react","version":"0.7.0","source":"https://fancy-ui.rama.app/docs/components/thinking-indicator?framework=react"}}