# Voice Orb

Voice Orb [#voice-orb]

**Voice Orb** is an animated canvas visualizer for voice agents. It reflects discrete agent states (`idle` / `connecting` / `listening` / `thinking` / `speaking` / `muted`) and reacts to input/output audio volume through `getInputVolume()` / `getOutputVolume()` callbacks. Visual state is fully decoupled from the audio source — the orb reads volume through the callbacks, which you can feed from [`useVoiceRecorder().getVolume()`](https://localmode.dev/docs/react) (mic input) or a Web Audio `AnalyserNode` (e.g. on the TTS output node).

Drive `state` from [`useLiveTranscribe()` / `useTurnTaker()`](https://localmode.dev/docs/react) and `getInputVolume` from `useVoiceRecorder().getVolume()`.

**When to use it:** the centerpiece of a hands-free voice-agent UI, showing the agent's state and "loudness" at a glance.

Preview [#preview]

```tsx
'use client';

import { useRef, useState } from 'react';
import { VoiceOrb, type VoiceOrbState } from '@/components/voice-orb';

/**
 * Demo for {@link VoiceOrb}. Cycles through agent states and feeds a simulated
 * volume so the listening/speaking reactivity is visible without microphone
 * permission. The real app passes `getInputVolume`/`getOutputVolume` backed by
 * a Web Audio `AnalyserNode` over `getUserMedia`.
 */
const STATES: VoiceOrbState[] = [
  'idle',
  'connecting',
  'listening',
  'thinking',
  'speaking',
  'muted',
];

export default function VoiceOrbDemo() {
  const [state, setState] = useState<VoiceOrbState>('listening');
  // A ref-driven oscillator stands in for an analyser's per-frame volume.
  const phaseRef = useRef(0);
  const fakeVolume = () => {
    phaseRef.current += 0.08;
    return (Math.sin(phaseRef.current) * 0.5 + 0.5) * 0.8;
  };

  return (
    <div className="flex flex-col items-center gap-4">
      <VoiceOrb
        state={state}
        size={140}
        getInputVolume={fakeVolume}
        getOutputVolume={fakeVolume}
      />
      <div className="flex flex-wrap justify-center gap-2">
        {STATES.map((s) => (
          <button
            key={s}
            type="button"
            onClick={() => setState(s)}
            className={
              'rounded-md border px-3 py-1 text-xs font-medium transition-colors ' +
              (state === s
                ? 'border-primary bg-primary text-primary-foreground'
                : 'border-border bg-background hover:bg-accent')
            }
          >
            {s}
          </button>
        ))}
      </div>
    </div>
  );
}
```

Installation [#installation]

```bash
npx shadcn@latest add @localmode/ui/audio/voice-orb
```

Data source & dependencies [#data-source--dependencies]

**Data source:** renders the `state` you pass and reads volume through callbacks — works with any backend. Recommended producer: `useLiveTranscribe` / `useTurnTaker` for agent state and `useVoiceRecorder().getVolume()` for input volume from `@localmode/react` (on-device, optional).

* `clsx` + `tailwind-merge` — via the shared `cn()` util
* No external visualizer dependency — pure Canvas 2D

Files installed [#files-installed]

* `voice-orb.tsx` — the component
* `lib/utils.ts` — the `cn()` helper (if not already present)

Props [#props]

**VoiceOrb**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `state` | `"muted" \| "speaking" \| "thinking" \| "listening" \| "connecting" \| "idle"` | `"idle"` | The current agent state. Drives the orb's base animation independently of any audio source. |
| `getInputVolume` | `function` | — | Returns the current input (microphone) volume in `[0, 1]`. Called on every animation frame. Wire it to `useVoiceRecorder().getVolume()` (or a local Web Audio `AnalyserNode` over `getUserMedia`) so the listening state pulses with real mic loudness. The orb never touches the audio source itself. |
| `getOutputVolume` | `function` | — | Returns the current output (agent speech) volume in `[0, 1]`. Drives the speaking state. Wire it to an `AnalyserNode` on the TTS output node. |
| `size` | `number` | `128` | Diameter in pixels. |
| `color` | `string` | `"var(--primary)"` | Core color (CSS color). |
| `glowColor` | `string` | `same as `color`` | Outer gradient/glow color (CSS color). |

Examples [#examples]

Driven by the recorder's live volume [#driven-by-the-recorders-live-volume]

```tsx
import { VoiceOrb } from '@/components/voice-orb';
import { useVoiceRecorder } from '@localmode/react';

export function Agent({ state }: { state: 'listening' | 'speaking' | 'idle' }) {
  const recorder = useVoiceRecorder();

  // The orb polls getInputVolume() on every animation frame; getVolume()
  // returns the live RMS input level in [0, 1] while recording (0 otherwise).
  return (
    <>
      <button onClick={recorder.isRecording ? () => recorder.stopRecording() : recorder.startRecording}>
        {recorder.isRecording ? 'Stop' : 'Listen'}
      </button>
      <VoiceOrb state={state} getInputVolume={recorder.getVolume} />
    </>
  );
}
```

For the speaking state, feed `getOutputVolume` from an `AnalyserNode` on your TTS output node — the agent's speech is not a microphone stream, so the recorder doesn't cover it.

Customization [#customization]

The orb draws to a single canvas; tune the per-state pulse speed and base radius in `STATE_CONFIG`, or swap the radial gradient for your brand colors via `color` / `glowColor` (both accept CSS `var(--…)` tokens, resolved against the live canvas). Because volume comes through callbacks, the orb never touches your audio graph — point it at input, output, or any custom amplitude source.