{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"voice-waveform","type":"registry:ui","title":"Voice Waveform","description":"An animated audio waveform — distinct bar motion for listening, recording, and the assistant speaking back.","author":"Scrim UI (https://scrimui.dev)","categories":["voice"],"docs":"https://scrimui.dev/components/voice-waveform","dependencies":[],"registryDependencies":[],"files":[{"path":"src/showcase/voice-waveform/voice-waveform.tsx","content":"\"use client\";\n\nimport * as React from \"react\";\n\n/* ------------------------------------------------------------------ */\n/* Types                                                               */\n/* ------------------------------------------------------------------ */\n\nexport type WaveformState = \"idle\" | \"listening\" | \"recording\" | \"speaking\";\n\nexport type VoiceWaveformProps = {\n  state?: WaveformState;\n  bars?: number;\n  className?: string;\n};\n\n/* Bars animate with the same keyframe; the state picks the speed,\n   opacity and color (color comes from the parent via currentColor). */\n\nconst DURATION: Record<WaveformState, number> = {\n  idle: 0,\n  listening: 1.1,\n  recording: 0.8,\n  speaking: 0.55,\n};\n\nconst OPACITY: Record<WaveformState, number> = {\n  idle: 0.3,\n  listening: 0.7,\n  recording: 1,\n  speaking: 1,\n};\n\n/* ------------------------------------------------------------------ */\n/* VoiceWaveform                                                       */\n/* ------------------------------------------------------------------ */\n\nexport function VoiceWaveform({\n  state = \"idle\",\n  bars = 24,\n  className = \"\",\n}: VoiceWaveformProps) {\n  const dur = DURATION[state];\n  const opacity = OPACITY[state];\n\n  return (\n    <>\n      <style>{`@keyframes aiui-wave{0%,100%{transform:scaleY(.25)}50%{transform:scaleY(1)}}`}</style>\n      <div className={`flex h-8 items-center gap-[2px] ${className}`} aria-hidden>\n        {Array.from({ length: bars }).map((_, i) => {\n          /* static bell shape: center bars taller than the edges */\n          const t = i / Math.max(bars - 1, 1);\n          /* Rounded, not raw: a full-precision float lands in the SSR HTML as\n             scaleY(0.6943240406445356), and the browser hands it back to\n             hydration as scaleY(0.694324) — a mismatch React reports as an\n             error. Three decimals is well below a visible difference. */\n          const base = Number((0.3 + 0.7 * Math.sin(Math.PI * t)).toFixed(3));\n          return (\n            <span\n              key={i}\n              className=\"w-[3px] rounded-full bg-current\"\n              style={{\n                height: \"100%\",\n                transform: `scaleY(${base})`,\n                transformOrigin: \"center\",\n                animation: dur\n                  ? `aiui-wave ${dur}s ease-in-out ${i * (dur / bars)}s infinite`\n                  : \"none\",\n                opacity,\n              }}\n            />\n          );\n        })}\n      </div>\n    </>\n  );\n}\n","type":"registry:ui","target":"components/ui/voice-waveform.tsx"}]}