{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"voice-conversation","type":"registry:ui","title":"Voice Conversation","description":"A voice conversation as it happens — who is speaking, the live transcript, and replay for any individual turn.","author":"Scrim UI (https://scrimui.dev)","categories":["voice"],"docs":"https://scrimui.dev/components/voice-conversation","dependencies":[],"registryDependencies":[],"files":[{"path":"src/showcase/voice-conversation/voice-conversation.tsx","content":"\"use client\";\n\nimport * as React from \"react\";\n\n/* ------------------------------------------------------------------ */\n/* Types                                                               */\n/* ------------------------------------------------------------------ */\n\nexport type VoiceTurn = {\n  id: string;\n  role: \"user\" | \"assistant\";\n  text: string;\n  time?: string;\n  speaking?: boolean;\n};\n\nexport type VoiceConversationProps = {\n  turns: VoiceTurn[];\n  onReplay?: (id: string) => void;\n  className?: string;\n};\n\n/* ------------------------------------------------------------------ */\n/* Icons                                                               */\n/* ------------------------------------------------------------------ */\n\nfunction UserIcon(props: React.SVGProps<SVGSVGElement>) {\n  return (\n    <svg\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth=\"2\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      width=\"14\"\n      height=\"14\"\n      {...props}\n    >\n      <circle cx=\"12\" cy=\"8\" r=\"4\" />\n      <path d=\"M4 21a8 8 0 0 1 16 0\" />\n    </svg>\n  );\n}\n\nfunction PlayIcon(props: React.SVGProps<SVGSVGElement>) {\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"currentColor\" width=\"11\" height=\"11\" {...props}>\n      <path d=\"M7 4.5v15a1 1 0 0 0 1.52.85l12-7.5a1 1 0 0 0 0-1.7l-12-7.5A1 1 0 0 0 7 4.5z\" />\n    </svg>\n  );\n}\n\n/* ------------------------------------------------------------------ */\n/* Speaking indicator                                                  */\n/* ------------------------------------------------------------------ */\n\nfunction SpeakingIcon() {\n  return (\n    <>\n      <style>{`@keyframes aiui-spk{0%,100%{transform:scaleY(.3)}50%{transform:scaleY(1)}}`}</style>\n      {/* role=\"img\" is what makes aria-label legal here: on a bare <span> with\n          no role, aria-label is a prohibited attribute and is ignored, so the\n          animated bars had no accessible name at all. */}\n      <span\n        role=\"img\"\n        aria-label=\"Speaking\"\n        className=\"inline-flex h-3.5 items-center gap-[2px] text-violet-500\"\n      >\n        {[0, 1, 2, 3].map((i) => (\n          <span\n            key={i}\n            className=\"w-[2px] rounded-full bg-current\"\n            style={{\n              height: \"100%\",\n              transform: \"scaleY(.3)\",\n              transformOrigin: \"center\",\n              animation: `aiui-spk 0.6s ease-in-out ${i * 0.12}s infinite`,\n            }}\n          />\n        ))}\n      </span>\n    </>\n  );\n}\n\n/* ------------------------------------------------------------------ */\n/* VoiceConversation                                                   */\n/* ------------------------------------------------------------------ */\n\nexport function VoiceConversation({\n  turns,\n  onReplay,\n  className = \"\",\n}: VoiceConversationProps) {\n  return (\n    <div className={`space-y-3 ${className}`}>\n      {turns.map((turn) => {\n        const user = turn.role === \"user\";\n        return (\n          <div key={turn.id} className={`flex items-start gap-3 ${user ? \"flex-row-reverse\" : \"\"}`}>\n            <span\n              className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-lg text-xs font-semibold ${\n                user\n                  ? \"bg-zinc-100 text-zinc-500 dark:bg-zinc-800 dark:text-zinc-400\"\n                  : \"bg-zinc-900 text-white dark:bg-zinc-100 dark:text-zinc-900\"\n              }`}\n            >\n              {user ? <UserIcon /> : \"AI\"}\n            </span>\n\n            <div\n              className={`min-w-0 max-w-[75%] rounded-2xl px-4 py-3 ${\n                user\n                  ? \"rounded-tr-md bg-zinc-900 text-white dark:bg-zinc-100 dark:text-zinc-900\"\n                  : \"rounded-tl-md border border-zinc-200 bg-zinc-50 dark:border-zinc-800 dark:bg-zinc-800/60\"\n              }`}\n            >\n              <div\n                className={`flex items-center gap-2 text-[11px] ${\n                  user ? \"text-zinc-300 dark:text-zinc-600\" : \"text-zinc-500 dark:text-zinc-400\"\n                }`}\n              >\n                <span className=\"font-medium\">{user ? \"You\" : \"Assistant\"}</span>\n                {turn.speaking && <SpeakingIcon />}\n                {turn.time && <span className=\"tabular-nums\">{turn.time}</span>}\n                {!turn.speaking && onReplay && (\n                  <button\n                    type=\"button\"\n                    onClick={() => onReplay(turn.id)}\n                    aria-label={`Replay ${user ? \"your\" : \"the assistant's\"} message`}\n                    className=\"rounded-md p-0.5 transition-colors hover:text-zinc-600 dark:hover:text-zinc-300\"\n                  >\n                    <PlayIcon />\n                  </button>\n                )}\n              </div>\n              <p className=\"mt-1 text-sm leading-6\">{turn.text}</p>\n            </div>\n          </div>\n        );\n      })}\n    </div>\n  );\n}\n","type":"registry:ui","target":"components/ui/voice-conversation.tsx"}]}