{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "elements-voice-conversation",
  "type": "registry:component",
  "title": "Voice conversation",
  "description": "A live call: the orb tracks your voice, the caption names the turn, the transcript follows.",
  "registryDependencies": [
    "https://r.assistant-ui.com/elements-surfaces.json",
    "https://r.assistant-ui.com/elements-range.json"
  ],
  "dependencies": [
    "lucide-react"
  ],
  "files": [
    {
      "type": "registry:component",
      "path": "components/assistant-ui/elements/voice-conversation.tsx",
      "sourcePath": "packages/ui/src/components/react/assistant-ui/elements/voice-conversation.tsx",
      "content": "\"use client\";\n\nimport type { ComponentProps } from \"react\";\nimport { MicIcon, MicOffIcon, PhoneOffIcon } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { ghostButton, mono, paper } from \"./surfaces\";\nimport { clamp } from \"../utils/range\";\n\nexport type VoiceMode = \"connecting\" | \"listening\" | \"thinking\" | \"speaking\";\n\nexport interface VoiceTurn {\n  id: string;\n  role: \"user\" | \"assistant\";\n  text: string;\n}\n\nconst CAPTION: Record<VoiceMode, string> = {\n  connecting: \"Connecting\",\n  listening: \"Listening\",\n  thinking: \"Thinking\",\n  speaking: \"Speaking\",\n};\n\nconst HINT: Record<VoiceMode, string> = {\n  connecting: \"Opening the mic\",\n  listening: \"Listening for you\",\n  thinking: \"Working on it\",\n  speaking: \"Playing the reply\",\n};\n\nexport function VoiceConversation({\n  mode,\n  amplitude,\n  transcript,\n  muted,\n  onToggleMute,\n  onInterrupt,\n  onEnd,\n  className,\n  ...props\n}: Omit<\n  ComponentProps<\"div\">,\n  | \"children\"\n  | \"mode\"\n  | \"amplitude\"\n  | \"transcript\"\n  | \"muted\"\n  | \"onToggleMute\"\n  | \"onInterrupt\"\n  | \"onEnd\"\n> & {\n  mode: VoiceMode;\n  amplitude: number;\n  transcript: readonly VoiceTurn[];\n  muted?: boolean;\n  onToggleMute?: () => void;\n  onInterrupt?: () => void;\n  onEnd?: () => void;\n}) {\n  const level = clamp(amplitude, 0, 1);\n  const active = mode === \"listening\" || mode === \"speaking\";\n  const canInterrupt = mode === \"speaking\" && onInterrupt !== undefined;\n\n  return (\n    <div\n      data-slot=\"voice-conversation\"\n      className={cn(\n        paper,\n        \"flex w-full max-w-xs flex-col items-center gap-4 rounded-[28px] px-5 py-5\",\n        className,\n      )}\n\n      {...props}\n    >\n      <button\n        type=\"button\"\n        onClick={onInterrupt}\n        disabled={!canInterrupt}\n        aria-label=\"Interrupt the assistant\"\n        className=\"focus-visible:ring-foreground/20 relative flex size-24 items-center justify-center rounded-full outline-none focus-visible:ring-1 disabled:cursor-default\"\n      >\n        <span\n          aria-hidden\n          className={cn(\n            \"absolute rounded-full transition-[transform,opacity] duration-200 ease-out motion-reduce:transition-none\",\n            mode === \"speaking\"\n              ? \"bg-blue-500/12 dark:bg-blue-400/15\"\n              : \"bg-foreground/[0.05]\",\n          )}\n          style={{\n            width: \"6rem\",\n            height: \"6rem\",\n            transform: `scale(${active ? 0.72 + level * 0.28 : 0.62})`,\n            opacity: active ? 1 : 0.5,\n          }}\n        />\n        <span\n          aria-hidden\n          className={cn(\n            \"absolute rounded-full transition-[transform,opacity] duration-150 ease-out motion-reduce:transition-none\",\n            mode === \"speaking\"\n              ? \"bg-blue-500/20 dark:bg-blue-400/25\"\n              : \"bg-foreground/[0.08]\",\n          )}\n          style={{\n            width: \"4.25rem\",\n            height: \"4.25rem\",\n            transform: `scale(${active ? 0.8 + level * 0.22 : 0.7})`,\n          }}\n        />\n        <span\n          aria-hidden\n          className={cn(\n            \"relative size-10 rounded-full transition-[transform,background-color] duration-150 ease-out motion-reduce:transition-none\",\n            mode === \"connecting\" && \"bg-foreground/20 animate-pulse\",\n            mode === \"listening\" && \"bg-foreground/80\",\n            mode === \"thinking\" && \"bg-foreground/30 animate-pulse\",\n            mode === \"speaking\" && \"bg-blue-500 dark:bg-blue-400\",\n          )}\n          style={{ transform: `scale(${active ? 0.9 + level * 0.2 : 0.85})` }}\n        />\n      </button>\n\n      <div className=\"flex flex-col items-center gap-1\">\n        <span className=\"text-[13.5px] font-medium\">{CAPTION[mode]}</span>\n        <span className={cn(mono, \"text-foreground/35\")}>\n          {muted ? \"Mic off\" : canInterrupt ? \"Tap to interrupt\" : HINT[mode]}\n        </span>\n      </div>\n\n      <div className=\"flex min-h-[4.5rem] w-full flex-col gap-1.5\">\n        {transcript.map((turn) => (\n          <div\n            key={turn.id}\n            className=\"fade-in slide-in-from-bottom-1 animate-in fill-mode-both flex gap-2 text-xs leading-relaxed duration-300\"\n          >\n            <span\n              className={cn(\n                mono,\n                \"w-8 shrink-0\",\n                turn.role === \"user\"\n                  ? \"text-foreground/30\"\n                  : \"text-blue-500/70 dark:text-blue-400/70\",\n              )}\n            >\n              {turn.role === \"user\" ? \"you\" : \"ai\"}\n            </span>\n            <span\n              className={cn(\n                \"min-w-0 flex-1 break-words\",\n                turn.role === \"user\"\n                  ? \"text-foreground/50\"\n                  : \"text-foreground/80\",\n              )}\n            >\n              {turn.text}\n            </span>\n          </div>\n        ))}\n      </div>\n\n      <div className=\"flex items-center gap-2\">\n        <button\n          type=\"button\"\n          aria-label={\n            muted ? \"Turn the microphone on\" : \"Turn the microphone off\"\n          }\n          aria-pressed={muted}\n          onClick={onToggleMute}\n          disabled={!onToggleMute}\n          className={cn(\n            ghostButton,\n            \"size-10 disabled:pointer-events-none disabled:opacity-30\",\n            muted && \"bg-foreground/[0.08] text-foreground/90\",\n          )}\n        >\n          {muted ? (\n            <MicOffIcon className=\"size-4\" />\n          ) : (\n            <MicIcon className=\"size-4\" />\n          )}\n        </button>\n        <button\n          type=\"button\"\n          aria-label=\"End the call\"\n          onClick={onEnd}\n          disabled={!onEnd}\n          className=\"flex size-10 items-center justify-center rounded-full bg-red-500/90 text-white transition-[opacity,scale] duration-150 hover:opacity-90 active:scale-[0.96] disabled:pointer-events-none disabled:opacity-30 motion-reduce:transition-none\"\n        >\n          <PhoneOffIcon className=\"size-4\" />\n        </button>\n      </div>\n    </div>\n  );\n}\n"
    }
  ]
}