{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"voice-call-controls","type":"registry:ui","title":"Voice Call Controls","description":"The chrome around a live voice call — an unmistakable mute toggle, a running call timer, and an end button that's always one tap away.","author":"Scrim UI (https://scrimui.dev)","categories":["voice"],"docs":"https://scrimui.dev/components/voice-call-controls","dependencies":[],"registryDependencies":[],"files":[{"path":"src/showcase/voice-call-controls/voice-call-controls.tsx","content":"\"use client\";\n\nimport * as React from \"react\";\n\n/**\n * The chrome around a live voice call.\n *\n * Voice input is a button; a voice *call* is a session, and a session needs\n * its own controls. Three things, no more: am I muted, how long has this run,\n * and how do I get out.\n *\n * **Mute is the trust control.** In a call that is always listening, the mic\n * toggle is the single thing a user checks before saying something private.\n * It is a toggle with an unmistakable state — a slashed mic and a label,\n * never an icon swap alone — and it stays in the same spot for the whole\n * call, because hunting for mute mid-sentence is the failure mode.\n *\n * **The clock proves the line is open.** A voice call has no scrollback; the\n * elapsed timer is the only persistent evidence the session is alive. When it\n * stops, the call has dropped whether the UI admits it or not.\n *\n * **End is red and requires no courage.** One tap, always visible, never\n * hidden behind a menu. A user who cannot find the exit will find it by\n * killing the tab, and takes the trust damage with them.\n *\n * What is deliberately not here: transcript, waveform, volume. Those belong\n * to the conversation surface above the bar; this is only the chrome.\n */\n\nexport type VoiceCallControlsProps = {\n  /** Whether the microphone is currently muted. */\n  muted?: boolean;\n  /** Fired when the mute toggle is pressed. */\n  onToggleMute?: () => void;\n  /** Seconds into the call — rendered as m:ss. */\n  elapsedSeconds?: number;\n  /** Fired when the end-call button is pressed. */\n  onEnd?: () => void;\n  className?: string;\n};\n\n/* ------------------------------------------------------------------ */\n/* Icons                                                               */\n/* ------------------------------------------------------------------ */\n\nfunction MicIcon() {\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" width=\"18\" height=\"18\">\n      <path d=\"M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z\" />\n      <path d=\"M19 10v2a7 7 0 0 1-14 0v-2\" />\n      <path d=\"M12 19v3\" />\n    </svg>\n  );\n}\n\nfunction MicOffIcon() {\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" width=\"18\" height=\"18\">\n      <line x1=\"2\" x2=\"22\" y1=\"2\" y2=\"22\" />\n      <path d=\"M18.89 13.23A7.12 7.12 0 0 0 19 12v-2\" />\n      <path d=\"M5 10v2a7 7 0 0 0 12 5\" />\n      <path d=\"M15 9.34V5a3 3 0 0 0-5.68-1.33\" />\n      <path d=\"M9 9v3a3 3 0 0 0 5.12 2.12\" />\n      <path d=\"M12 19v3\" />\n    </svg>\n  );\n}\n\nfunction PhoneOffIcon() {\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" width=\"18\" height=\"18\">\n      <path d=\"M10.68 13.31a16 16 0 0 0 3.41 2.6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.42 19.42 0 0 1-3.33-2.67m-2.67-3.34a19.79 19.79 0 0 1-3.07-8.63A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91\" />\n      <line x1=\"22\" x2=\"2\" y1=\"2\" y2=\"22\" />\n    </svg>\n  );\n}\n\nfunction formatElapsed(total: number): string {\n  const m = Math.floor(total / 60);\n  const s = total % 60;\n  return `${m}:${s.toString().padStart(2, \"0\")}`;\n}\n\n/* ------------------------------------------------------------------ */\n/* VoiceCallControls                                                   */\n/* ------------------------------------------------------------------ */\n\nexport function VoiceCallControls({\n  muted = false,\n  onToggleMute,\n  elapsedSeconds = 0,\n  onEnd,\n  className = \"\",\n}: VoiceCallControlsProps) {\n  return (\n    <div\n      className={`flex items-center justify-between rounded-full border border-zinc-200 bg-white px-3 py-2 dark:border-zinc-800 dark:bg-zinc-900 ${className}`}\n      role=\"toolbar\"\n      aria-label=\"Voice call controls\"\n    >\n      <button\n        type=\"button\"\n        onClick={onToggleMute}\n        aria-pressed={muted}\n        className={`flex h-10 items-center gap-2 rounded-full px-3 text-sm font-medium transition-colors ${\n          muted\n            ? \"bg-zinc-900 text-white dark:bg-zinc-100 dark:text-zinc-900\"\n            : \"text-zinc-700 hover:bg-zinc-100 dark:text-zinc-200 dark:hover:bg-zinc-800\"\n        }`}\n      >\n        {muted ? <MicOffIcon /> : <MicIcon />}\n        {muted ? \"Muted\" : \"Mute\"}\n      </button>\n\n      <span\n        className=\"text-sm tabular-nums text-zinc-500 dark:text-zinc-400\"\n        aria-label={`Call duration ${formatElapsed(elapsedSeconds)}`}\n      >\n        {formatElapsed(elapsedSeconds)}\n      </span>\n\n      <button\n        type=\"button\"\n        onClick={onEnd}\n        className=\"flex h-10 items-center gap-2 rounded-full bg-red-600 px-4 text-sm font-medium text-white transition-colors hover:bg-red-500\"\n      >\n        <PhoneOffIcon />\n        End\n      </button>\n    </div>\n  );\n}\n","type":"registry:ui","target":"components/ui/voice-call-controls.tsx"}]}