Every orb takes the same two inputs: state (the assistant lifecycle) and levelRef (a live 0..1 audio amplitude). These recipes show how to feed both from real voice stacks using the typed copy-paste adapters in src/registry/lib. The adapters type each provider surface structurally, so nothing here adds an SDK dependency to your bundle until you install the one you actually use.
Plain microphone
No dependencies
The simplest wiring: drive state yourself and feed levelRef from the bundled use-audio-level hook, which reads the microphone through a shared AnalyserNode. When levelRef is negative the orb falls back to its procedural animation, so the orb keeps moving even before the user grants mic access.
Copy the adapter from src/registry/lib/use-audio-level.ts into your project first.
createVapiAdapter subscribes to the Vapi web client events: call-start and call-end drive the lifecycle, speech-start and speech-end mark when the assistant is speaking, and volume-level (already normalized 0..1) feeds levelRef directly. The client is typed structurally, so the adapter compiles without the SDK installed.
Copy the adapter from src/registry/lib/create-vapi-adapter.ts into your project first.
'use client';
import { useEffect, useMemo, useSyncExternalStore } from 'react';
import Vapi from '@vapi-ai/web';
import type { OrbState } from '@/registry/lib/orb-state';
import { createVapiAdapter } from '@/registry/lib/create-vapi-adapter';
import { PulseOrb } from '@/registry/orbe/pulse-orb/pulse-orb';
const vapi = new Vapi(process.env.NEXT_PUBLIC_VAPI_PUBLIC_KEY!);
const serverState = (): OrbState => 'idle';
export const VapiOrb = () => {
const adapter = useMemo(() => createVapiAdapter(vapi), []);
useEffect(() => adapter.dispose, [adapter]);
const state = useSyncExternalStore(adapter.subscribe, adapter.getState, serverState);
const toggle = () => {
if (state === 'idle' || state === 'error') {
adapter.setState('connecting');
void vapi.start(process.env.NEXT_PUBLIC_VAPI_ASSISTANT_ID!);
} else {
void vapi.stop();
}
};
return (
<div className="flex flex-col items-center gap-4">
<PulseOrb state={state} levelRef={adapter.levelRef} />
<button type="button" onClick={toggle}>
{state === 'idle' || state === 'error' ? 'Start call' : 'End call'}
</button>
</div>
);
};
ElevenLabs Agents
npm i @elevenlabs/react
createElevenLabsAdapter exposes a callbacks object you spread into useConversation: onConnect, onDisconnect and onError drive the lifecycle and onModeChange switches between listening and speaking. Once attached, it polls getInputVolume while listening and getOutputVolume while speaking in a requestAnimationFrame loop, smoothing the value into levelRef with approach().
Copy the adapter from src/registry/lib/create-elevenlabs-adapter.ts into your project first.
useVoiceAssistant already exposes the agent lifecycle, so mapAgentState translates its AgentState to the orb contract: disconnected becomes idle, connecting and initializing become connecting, and listening, thinking and speaking pass straight through. createLiveKitAdapter hangs an AnalyserNode off the agent audio track and writes the smoothed amplitude to levelRef.
Copy the adapter from src/registry/lib/create-livekit-adapter.ts into your project first.
createRealtimeAdapter works on the raw WebRTC session: point pc.ontrack at handleTrack so the remote audio track feeds levelRef through an AnalyserNode, and forward data channel messages to handleServerEvent, which maps server events (speech_started, response.created, output_audio_buffer.started and stopped, response.done, error) to orb states.
Copy the adapter from src/registry/lib/create-realtime-adapter.ts into your project first.