# Agent Orb

> Calm, softly glassy voice-agent sphere drawn in a shader: slow colour clouds that breathe when idle, swell with audio and turn while thinking.

- **Slug:** `agent-orb` · **Category:** voice · **Status:** beta
- **Import:** `import { AgentOrb } from "@kura-ui/react/agent-orb";`
- **Docs:** https://kuro.abstergo.fyi/components/agent-orb
- **Registry item:** https://kuro.abstergo.fyi/r/agent-orb.json
- **Tags:** voice, orb, agent, audio, webgl, shader, assistant, speaking, listening

## Install

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

## When to use

- The central visual of a voice agent or call screen: idle, listening, thinking, speaking, error.
- Showing that an assistant is hearing the user (mic level) or talking back (TTS level).
- A 'talk to the agent' entry point inside a button or a card.
- An ambient, branded presence for an AI assistant in an empty state.

## When not to use

- Precise audio feedback such as a recording meter or waveform — use audio-waveform or voice-dictation.
- Text chat 'thinking' feedback inside a message list — use thinking-indicator or shimmer-text.
- More than one per view: the orb is the accent element; repeat it and it stops meaning anything.

## Props

| Prop | Type | Default | Required | Description |
| --- | --- | --- | --- | --- |
| `state` | `"idle" \| "listening" \| "thinking" \| "speaking" \| "error"` | `"idle"` | no | What the agent is doing. idle breathes slowly, listening swells with the input level, thinking turns and twists the clouds, speaking lifts with the output level, error shifts to the danger colour and calms down. |
| `colors` | `"signal" \| "pearl" \| "graphite" \| "ocean" \| "amber" \| readonly [string, string, string, string]` | `"signal"` | no | A palette preset, or four CSS colours `[deep, mid, bright, glint]` = densest clouds, body, light between clouds, rim/sheen (OKLCH, hex, `var(--token)`, `color-mix()`…). `signal` follows the theme: pearl with blue clouds on light pages, graphite with blue clouds on dark. `pearl` and `graphite` are neutral, `ocean` is a deeper blue, `amber` a soft warm option. |
| `analyser` | `AudioAnalyser` | — | no | From `useAudioAnalyser`. Its level drives the orb while listening or speaking. Read every frame on the GPU loop — no React re-renders. |
| `inputLevel` | `number` | — | no | Microphone level 0–1 used while `listening`. Overrides `analyser`. |
| `outputLevel` | `number` | — | no | Agent voice level 0–1 used while `speaking`. Overrides `analyser`. |
| `label` | `string` | — | no | Accessible text for the state. Default: Voice agent idle / Listening / Thinking / Speaking / Voice agent error. |
| `announce` | `boolean` | `true` | no | Render the label in a polite live region. Set false when several orbs are on screen or a parent already announces the state. |
| `maxDpr` | `number` | `2` | no | Device-pixel-ratio cap. |

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

## Anatomy

- **AgentOrb** (`data-slot="agent-orb"`) — Square root (`aspect-square`); size it with a width class. `data-state` mirrors `state`.
- **ShaderCanvas** (`data-slot="agent-orb-canvas"`) — The WebGL sphere (aria-hidden). The sphere fills ~84% of the box; the rest holds a soft contact shadow on light pages and a faint halo on dark ones.
- **Fallback** (`data-slot="agent-orb-fallback"`) — CSS gradient sphere in the same palette, used when WebGL2 is unavailable; its clouds turn slowly while thinking.

## Accessibility

- Role: `status`
- The canvas is decorative (aria-hidden). The state is exposed as visually hidden text in a role=status live region, announced politely when it changes.
- Audio levels are never announced. Pass `label` to describe custom situations (e.g. “Muted”).
- If the orb is inside a button, give the button its own aria-label and set `announce={false}` to avoid double announcements.
- Reduced motion: No breathing, swirl, flow or audio reaction: a still frame per state. State changes cross-fade colour and brightness over ~0.4s, then rendering stops.

## Notes for agents

- Give it a width (`className="size-48"` or `w-full max-w-64`); it is square. The sphere fills ~84% so the shadow/halo has room — do not clip it with overflow-hidden.
- Keep the default `signal` palette when the orb stands for your AI: it is the only blue on a neutral page. Use `pearl` or `graphite` for a quieter, brand-neutral orb.
- For real audio: `const mic = useAudioAnalyser({ source: micStream })` for listening and a second analyser on the TTS <audio> element for speaking; switch `analyser` with the state. For demos use `useAudioAnalyser({ simulate: "speech" })`.
- `inputLevel`/`outputLevel` are fine for levels you already have (e.g. from a WebRTC SDK) at ≤ 30 updates/s; the shader smooths them.
- Changing `state` never restarts the animation: uniforms ease toward the new state, so you can flip states as fast as your agent emits events.
- Only one orb per view should be animated; for lists of agents use a static avatar.

## Examples

### Idle orb (minimal)

Size it with a class; it breathes slowly.

```tsx
import { AgentOrb } from "@kura-ui/react/agent-orb";

export function AgentOrbMinimal() {
  return <AgentOrb className="size-48" />;
}
```

### States, palettes and audio (full)

Every state and preset, driven by a simulated speech analyser.

```tsx
"use client";

import * as React from "react";
import { useAudioAnalyser } from "@kura-ui/react/hooks";
import { Button } from "@kura-ui/react/button";
import { AgentOrb, type AgentOrbPreset, type AgentOrbState } from "@kura-ui/react/agent-orb";

const states: AgentOrbState[] = ["idle", "listening", "thinking", "speaking", "error"];

const presets: AgentOrbPreset[] = ["signal", "pearl", "graphite", "ocean", "amber"];

export function AgentOrbFull() {
  const [state, setState] = React.useState<AgentOrbState>("speaking");
  const [preset, setPreset] = React.useState<AgentOrbPreset>("signal");
  // Simulated audio for the demo; pass `source: micStream` in a real app.
  const analyser = useAudioAnalyser({ simulate: "speech" });
  return (
    <div className="flex w-full max-w-md flex-col items-center gap-5">
      <AgentOrb state={state} colors={preset} analyser={analyser} className="size-64" />
      <div
        role="radiogroup"
        aria-label="Agent state"
        className="flex flex-wrap justify-center gap-1"
      >
        {states.map((s) => (
          <Button
            key={s}
            size="sm"
            variant={s === state ? "secondary" : "ghost"}
            role="radio"
            aria-checked={s === state}
            onClick={() => setState(s)}
            className="capitalize"
          >
            {s}
          </Button>
        ))}
      </div>
      <div role="radiogroup" aria-label="Palette" className="flex gap-1">
        {presets.map((p) => (
          <Button
            key={p}
            size="xs"
            variant={p === preset ? "secondary" : "ghost"}
            role="radio"
            aria-checked={p === preset}
            onClick={() => setPreset(p)}
            className="capitalize"
          >
            {p}
          </Button>
        ))}
      </div>
    </div>
  );
}
```

### All five states (state)

idle, listening, thinking, speaking and error side by side.

```tsx
"use client";

import { useAudioAnalyser } from "@kura-ui/react/hooks";
import { AgentOrb, type AgentOrbState } from "@kura-ui/react/agent-orb";

const states: AgentOrbState[] = ["idle", "listening", "thinking", "speaking", "error"];

export function AgentOrbStates() {
  const analyser = useAudioAnalyser({ simulate: "speech" });
  return (
    <div className="grid w-full grid-cols-5 gap-2">
      {states.map((s) => (
        <figure key={s} className="flex flex-col items-center gap-1">
          <AgentOrb state={s} analyser={analyser} announce={false} className="w-full max-w-32" />
          <figcaption className="text-kura-fg-subtle text-xs capitalize">{s}</figcaption>
        </figure>
      ))}
    </div>
  );
}
```

### Voice session card (composition)

A scripted call with captions, ShimmerText while thinking and IconButton controls.

```tsx
"use client";

import { Mic, MicOff, PhoneOff } from "lucide-react";
import * as React from "react";
import { useAudioAnalyser } from "@kura-ui/react/hooks";
import { IconButton } from "@kura-ui/react/icon-button";
import { ShimmerText } from "@kura-ui/react/shimmer-text";
import { AgentOrb, type AgentOrbState } from "@kura-ui/react/agent-orb";

const script: Array<{ state: AgentOrbState; ms: number; caption: string }> = [
  { state: "listening", ms: 3200, caption: "Listening…" },
  { state: "thinking", ms: 2400, caption: "Checking your calendar…" },
  { state: "speaking", ms: 4200, caption: "You're free after 3pm on Thursday." },
];

export function AgentOrbVoiceSession() {
  const [step, setStep] = React.useState(0);
  const [muted, setMuted] = React.useState(false);
  const analyser = useAudioAnalyser({ simulate: "speech" });
  const current = script[step % script.length]!;
  React.useEffect(() => {
    const id = setTimeout(() => setStep((s) => s + 1), current.ms);
    return () => clearTimeout(id);
  }, [current]);
  const state: AgentOrbState = muted && current.state === "listening" ? "idle" : current.state;
  return (
    <div className="flex w-full max-w-sm flex-col items-center gap-4 rounded-kura-2xl bg-kura-surface p-6 shadow-kura-md">
      <AgentOrb
        state={state}
        analyser={analyser}
        label={muted ? "Muted" : undefined}
        className="size-44"
      />
      <p className="flex h-10 items-center text-center text-kura-fg-muted text-sm" aria-hidden>
        {state === "thinking" ? <ShimmerText>{current.caption}</ShimmerText> : current.caption}
      </p>
      <div className="flex items-center gap-2">
        <IconButton
          label={muted ? "Unmute" : "Mute"}
          variant="secondary"
          size="lg"
          aria-pressed={muted}
          icon={muted ? <MicOff /> : <Mic />}
          onClick={() => setMuted((m) => !m)}
        />
        <IconButton label="End call" variant="danger" size="lg" icon={<PhoneOff />} />
      </div>
    </div>
  );
}
```

## Dependencies

- npm: none
- Kura registry items: `kura-theme`, `kura-merge-refs`, `kura-shader-color`, `shader-canvas`, `use-audio-analyser`, `use-reduced-motion`, `utils`
- Related: `voice-dictation`, `audio-waveform`, `thinking-indicator`, `shader-canvas`
