# Thinking Indicator

> Compact AI “thinking” indicators — dots, pulse, orbit, wave, or cycling status text — inside a polite live region.

- **Slug:** `thinking-indicator` · **Category:** feedback · **Status:** beta
- **Import:** `import { ThinkingIndicator } from "@kura-ui/react/thinking-indicator";`
- **Docs:** https://kuro.abstergo.fyi/components/thinking-indicator
- **Registry item:** https://kuro.abstergo.fyi/r/thinking-indicator.json
- **Tags:** loading, thinking, typing, dots, status, indicator, pending, ai

## Install

```bash
# shadcn registry (copies source into your project)
npx shadcn@latest add https://kuro.abstergo.fyi/r/thinking-indicator.json
# Kura CLI (same, plus dependency resolution and --json output)
npx @kura-ui/cli add thinking-indicator
# npm package
npm install @kura-ui/react
```

## When to use

- 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.

## When not to use

- 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.

## Props

| Prop | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `variant` | `"dots" \| "pulse" \| "orbit" \| "wave" \| "text"` | `"dots"` | no | dots: three bouncing dots (staggered spring). pulse: a breathing dot with an expanding ring. orbit: two dots chasing round a track. wave: four equaliser bars. text: a pulse plus shimmering status text that cycles through `messages`. |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | no | Dot size 4 / 6 / 8 px; text 12 / 14 / 15 px. |
| `label` | `string` | `"Thinking"` | no | Accessible status text. Announced once when the indicator mounts or the label changes — never on each animation cycle. |
| `messages` | `readonly string[]` | — | no | Status lines for the text variant. Several lines cycle with a cross-fade (and only `label` is announced); a single line is shown and announced as is — update it as the agent progresses. |
| `interval` | `number` | `2400` | no | Milliseconds each message stays up when cycling (minimum 600). |

Native attributes of the root element, `className` and `ref` are passed through.

## Anatomy

- **ThinkingIndicator** (`data-slot="thinking-indicator"`) — Inline root with role=status. `data-variant` and `data-size` mirror the props. Colour is currentColor (text-kura-accent by default).
- **Shape** (`data-slot="thinking-indicator-shape"`) — The animated glyph (aria-hidden).
- **Text** (`data-slot="thinking-indicator-text"`) — Text variant only: all messages stacked in one grid cell (width = widest message, so no layout shift); the active one has data-active=true.

## Accessibility

- Role: `status`
- role=status + aria-live=polite: screen readers hear the label once, politely, not a stream of updates.
- Shapes are aria-hidden; a visually hidden label carries the meaning.
- When several messages cycle, the cycling text is aria-hidden and only `label` is announced, so the live region does not spam.
- Colour is not the only signal: the text variant and the label describe the state.
- Reduced motion: Loops stop. Dots show three static dots of rising opacity, the pulse is a still dot, orbit freezes with both dots visible, wave shows static bars of varied heights. Text still cycles but with an opacity cross-fade only (no blur, no rise, no shimmer).

## Notes for agents

- Recolour with a text class (`className="text-kura-fg-subtle"`); shapes use currentColor. Keep the jade default when it means “the model is working”.
- Replace the indicator with the streamed message once the first token arrives; do not show both.
- For real progress, pass one message and update it (`messages={[step]}`) — each change is announced. For ambient variety, pass several and let them cycle.
- Keep messages short (≤ 5 words) and end with an ellipsis.

## Examples

### Thinking dots (minimal)

```tsx
import { ThinkingIndicator } from "@kura-ui/react/thinking-indicator";

export function ThinkingIndicatorMinimal() {
  return <ThinkingIndicator />;
}
```

### Every variant and size (full)

dots, pulse, orbit and wave at sm/md/lg, cycling text, and a muted recolour.

```tsx
import * as React from "react";
import {
  ThinkingIndicator,
  type ThinkingIndicatorSize,
  type ThinkingIndicatorVariant,
} from "@kura-ui/react/thinking-indicator";

const variants: ThinkingIndicatorVariant[] = ["dots", "pulse", "orbit", "wave"];

const sizes: ThinkingIndicatorSize[] = ["sm", "md", "lg"];

export function ThinkingIndicatorFull() {
  return (
    <div className="flex w-full max-w-lg flex-col gap-6">
      <div className="grid grid-cols-[auto_repeat(3,1fr)] items-center gap-x-6 gap-y-4 text-kura-fg-subtle text-xs">
        <span />
        {sizes.map((s) => (
          <span key={s} className="font-kura-mono">
            {s}
          </span>
        ))}
        {variants.map((v) => (
          <React.Fragment key={v}>
            <span className="font-kura-mono">{v}</span>
            {sizes.map((s) => (
              <span key={s} className="flex h-6 items-center">
                <ThinkingIndicator variant={v} size={s} label={`Thinking (${v}, ${s})`} />
              </span>
            ))}
          </React.Fragment>
        ))}
      </div>
      <div className="flex flex-col gap-3 border-kura-border-subtle border-t pt-5">
        <ThinkingIndicator
          variant="text"
          label="Researching"
          messages={["Searching the web…", "Reading 4 files…", "Comparing 12 sources…"]}
          interval={2200}
        />
        <ThinkingIndicator variant="dots" size="sm" className="text-kura-fg-subtle" />
      </div>
    </div>
  );
}
```

### Pending assistant message (composition)

Text variant inside a message bubble with a Stop Button.

```tsx
"use client";

import { Bot } from "lucide-react";
import * as React from "react";
import { Button } from "@kura-ui/react/button";
import { ThinkingIndicator } from "@kura-ui/react/thinking-indicator";

export function ThinkingIndicatorInMessage() {
  const [stopped, setStopped] = React.useState(false);
  return (
    <div className="flex w-full max-w-md items-start gap-3">
      <span className="flex size-7 shrink-0 items-center justify-center rounded-full bg-kura-accent-subtle text-kura-accent-text">
        <Bot className="size-3.5" aria-hidden />
      </span>
      <div className="flex min-w-0 flex-1 flex-col gap-2 rounded-kura-xl bg-kura-surface px-4 py-3 shadow-kura-sm">
        {stopped ? (
          <p className="text-kura-fg-muted text-sm">Stopped. Ask again whenever you're ready.</p>
        ) : (
          <ThinkingIndicator
            variant="text"
            label="Assistant is working"
            messages={["Planning the migration…", "Reading schema.prisma…", "Drafting SQL…"]}
          />
        )}
        <div className="flex justify-end">
          <Button size="xs" variant="ghost" onClick={() => setStopped((s) => !s)}>
            {stopped ? "Retry" : "Stop"}
          </Button>
        </div>
      </div>
    </div>
  );
}
```

## Dependencies

- npm: none
- Kura registry items: `kura-theme`, `shimmer-text`, `utils`
- Related: `shimmer-text`, `spinner`, `border-beam`, `agent-orb`, `streaming-text`, `message`
