{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "voice-input",
  "title": "Voice Input",
  "description": "A voice recorder pill with animated level bars and a live timer.",
  "dependencies": [
    "lucide-react"
  ],
  "files": [
    {
      "path": "components/spectrumui/blocks/ai-assistants/voice-input.tsx",
      "content": "'use client';\n\nimport { useEffect, useState } from 'react';\nimport { Check, Mic, X } from 'lucide-react';\nimport { cn } from '@/lib/utils';\n\nconst KEYFRAMES = `\n@keyframes su-eq { 0%, 100% { transform: scaleY(0.3) } 50% { transform: scaleY(1) } }\n`;\n\nconst BAR_COUNT = 18;\nconst BAR_SEED = Array.from({ length: BAR_COUNT }, (_, i) => ({\n  delay: (i * 67) % 480,\n  duration: 640 + ((i * 131) % 420),\n  height: 8 + ((i * 53) % 12),\n}));\n\nexport type VoiceInputState = 'idle' | 'recording' | 'processing';\n\nexport interface VoiceInputProps {\n  state?: VoiceInputState;\n  onStart?: () => void;\n  onConfirm?: () => void;\n  onCancel?: () => void;\n  className?: string;\n}\n\nfunction useRecordingClock(active: boolean) {\n  const [ds, setDs] = useState(0);\n  useEffect(() => {\n    if (!active) {\n      setDs(0);\n      return;\n    }\n    const t = setInterval(() => setDs((d) => d + 1), 100);\n    return () => clearInterval(t);\n  }, [active]);\n  const total = ds / 10;\n  return `0:${String(Math.floor(total)).padStart(2, '0')}`;\n}\n\nexport function VoiceInput({\n  state: stateProp,\n  onStart,\n  onConfirm,\n  onCancel,\n  className,\n}: VoiceInputProps) {\n  const [stateState, setStateState] = useState<VoiceInputState>('idle');\n  const state = stateProp !== undefined ? stateProp : stateState;\n  const clock = useRecordingClock(state === 'recording');\n\n  function start() {\n    setStateState('recording');\n    onStart?.();\n  }\n  function confirm() {\n    setStateState('processing');\n    onConfirm?.();\n  }\n  function cancel() {\n    setStateState('idle');\n    onCancel?.();\n  }\n\n  return (\n    <div\n      className={cn(\n        'flex h-11 w-fit items-center gap-2 rounded-full border border-black/[0.08] bg-white py-1.5 pl-1.5 pr-2 shadow-xs dark:border-white/[0.09] dark:bg-[#0B0B0D]',\n        className,\n      )}\n    >\n      <style dangerouslySetInnerHTML={{ __html: KEYFRAMES }} />\n\n      {state === 'idle' && (\n        <>\n          <button\n            type=\"button\"\n            aria-label=\"Start recording\"\n            onClick={start}\n            className=\"grid size-8 place-items-center rounded-full bg-neutral-900 text-white transition-transform duration-[160ms] ease-[cubic-bezier(0.23,1,0.32,1)] active:scale-[0.92] focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-neutral-400 dark:bg-neutral-100 dark:text-neutral-900\"\n          >\n            <Mic className=\"size-3.5\" />\n          </button>\n          <span className=\"pr-1.5 text-[12.5px] text-neutral-400 dark:text-neutral-500\">\n            Tap to speak\n          </span>\n        </>\n      )}\n\n      {state === 'recording' && (\n        <>\n          <button\n            type=\"button\"\n            aria-label=\"Cancel recording\"\n            onClick={cancel}\n            className=\"grid size-8 shrink-0 place-items-center rounded-full text-neutral-400 transition-[color,background-color,transform] duration-150 hover:bg-black/[0.04] hover:text-neutral-700 active:scale-[0.92] focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-neutral-400 dark:hover:bg-white/[0.06] dark:hover:text-neutral-200\"\n          >\n            <X className=\"size-3.5\" />\n          </button>\n\n          <span aria-hidden className=\"flex h-6 items-center gap-[2.5px] px-1\">\n            {BAR_SEED.map((bar, index) => (\n              <span\n                key={index}\n                className=\"w-[2.5px] rounded-full bg-neutral-800 motion-reduce:!animate-none motion-reduce:!scale-y-50 dark:bg-neutral-200\"\n                style={{\n                  height: bar.height,\n                  animation: `su-eq ${bar.duration}ms ease-in-out ${bar.delay}ms infinite`,\n                  transformOrigin: 'center',\n                }}\n              />\n            ))}\n          </span>\n\n          <span\n            role=\"timer\"\n            aria-label={`Recording ${clock}`}\n            className=\"font-mono text-[11.5px] tabular-nums text-neutral-500 dark:text-neutral-400\"\n          >\n            {clock}\n          </span>\n\n          <button\n            type=\"button\"\n            aria-label=\"Finish recording\"\n            onClick={confirm}\n            className=\"grid size-8 shrink-0 place-items-center rounded-full bg-neutral-900 text-white transition-transform duration-[160ms] ease-[cubic-bezier(0.23,1,0.32,1)] active:scale-[0.92] focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-neutral-400 dark:bg-neutral-100 dark:text-neutral-900\"\n          >\n            <Check className=\"size-3.5\" />\n          </button>\n        </>\n      )}\n\n      {state === 'processing' && (\n        <>\n          <span aria-hidden className=\"flex h-8 items-center gap-[2.5px] px-2 opacity-50\">\n            {BAR_SEED.slice(0, 10).map((bar, index) => (\n              <span\n                key={index}\n                className=\"w-[2.5px] scale-y-[0.35] rounded-full bg-neutral-800 dark:bg-neutral-200\"\n                style={{ height: bar.height }}\n              />\n            ))}\n          </span>\n          <span className=\"pr-1.5 text-[12.5px] text-neutral-400 dark:text-neutral-500\" role=\"status\">\n            Transcribing…\n          </span>\n        </>\n      )}\n    </div>\n  );\n}\n\nexport default VoiceInput;\n",
      "type": "registry:block",
      "target": "components/spectrumui/blocks/ai-assistants/voice-input.tsx"
    }
  ],
  "type": "registry:block"
}
