{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "voice-input",
  "title": "Voice Input",
  "description": "A tactile recording control with a recessed waveform, red playhead, and pause control.",
  "dependencies": [
    "@solar-icons/react@^2.2.0",
    "cn@^0.3.0",
    "motion@^13.4.0"
  ],
  "registryDependencies": [
    "button",
    "https://pastaui.com/r/pastaui-styles.json"
  ],
  "files": [
    {
      "path": "src/components/ai/voice-input.tsx",
      "content": "import { Microphone2Icon as MicIcon } from '@solar-icons/react/linear/microphone-2'\nimport { PauseIcon } from '@solar-icons/react/bold/pause'\nimport { ArrowUpIcon } from '@solar-icons/react/linear/arrow-up'\nimport { TrashBinMinimalisticIcon as TrashIcon } from '@solar-icons/react/linear/trash-bin-minimalistic'\nimport { AnimatePresence, MotionConfig, motion } from 'motion/react'\nimport * as React from 'react'\nimport { useState } from 'react'\n\nimport { Button } from '@/components/ui/button'\nimport { cn } from 'cn'\n\ninterface VoiceInputProps extends Omit<React.ComponentProps<'div'>, 'onChange'> {\n  duration?: string\n  onCancel?: () => void\n  onRecordingChange?: (recording: boolean) => void\n  onSend?: () => void\n  placeholder?: string\n  recording?: boolean\n  transcript?: string\n}\n\nconst waveform = [\n  32, 23, 10, 54, 54, 46, 32, 23, 12, 55, 98, 46, 54, 33, 23, 34, 54, 46, 32, 23, 13, 45, 64, 54,\n]\n\nconst ease = [0.23, 1, 0.32, 1] as const\n\nconst primaryButton =\n  'rounded-full bg-primary bg-linear-to-b from-[color-mix(in_oklch,var(--primary),var(--primary-foreground)_30%)] to-primary text-primary-foreground shadow-[inset_0_1px_0_rgba(255,255,255,0.14),0_1px_2px_rgba(0,0,0,0.18)] hover:brightness-110 dark:bg-none'\n\nfunction VoiceInput({\n  className,\n  duration = '00:00',\n  onCancel,\n  onRecordingChange,\n  onSend,\n  placeholder = 'Tap the microphone and start speaking',\n  recording,\n  transcript,\n  ...props\n}: VoiceInputProps) {\n  const [internalRecording, setInternalRecording] = useState(false)\n  const isRecording = recording ?? internalRecording\n  const hasTake = Boolean(transcript) || duration !== '00:00'\n\n  function toggleRecording() {\n    const next = !isRecording\n    if (recording === undefined) setInternalRecording(next)\n    onRecordingChange?.(next)\n  }\n\n  return (\n    <MotionConfig reducedMotion=\"user\">\n      <div\n        data-recording={isRecording}\n        data-slot=\"voice-input\"\n        className={cn(\n          'w-full rounded-[16px] bg-muted p-4 text-sm font-[450] tracking-[-0.05px] text-muted-foreground shadow-card',\n          className,\n        )}\n        {...props}\n      >\n        <p aria-live=\"polite\" className=\"min-h-12 px-1 pb-3 leading-6\">\n          {transcript ? (\n            <AnimatePresence initial={false}>\n              {transcript.split(' ').map((word, index) => (\n                <motion.span\n                  animate={{ opacity: 1, filter: 'blur(0px)' }}\n                  initial={{ opacity: 0, filter: 'blur(3px)' }}\n                  key={`${index}-${word}`}\n                  transition={{ duration: 0.3, ease }}\n                >\n                  {word}{' '}\n                </motion.span>\n              ))}\n            </AnimatePresence>\n          ) : (\n            <span>{isRecording ? 'Listening…' : placeholder}</span>\n          )}\n        </p>\n        <div className=\"flex items-center gap-4\">\n          <Button\n            aria-label={\n              isRecording ? 'Pause recording' : hasTake ? 'Resume recording' : 'Start recording'\n            }\n            aria-pressed={isRecording}\n            type=\"button\"\n            variant=\"default\"\n            size=\"icon\"\n            className={cn('size-12 shrink-0', primaryButton)}\n            onClick={toggleRecording}\n          >\n            <span className=\"grid *:col-start-1 *:row-start-1 *:transition-[opacity,scale] *:duration-150 *:ease-[cubic-bezier(0.23,1,0.32,1)] motion-reduce:*:transition-opacity\">\n              <MicIcon\n                aria-hidden=\"true\"\n                className={cn('size-5', isRecording && 'scale-75 opacity-0')}\n              />\n              <PauseIcon\n                aria-hidden=\"true\"\n                className={cn('size-5', !isRecording && 'scale-75 opacity-0')}\n              />\n            </span>\n          </Button>\n          <div\n            aria-hidden=\"true\"\n            className=\"relative h-20 min-w-0 flex-1 overflow-hidden rounded-[10px] bg-muted\"\n          >\n            <div\n              className=\"absolute inset-x-0 inset-y-[24%]\"\n              style={{ maskImage: 'linear-gradient(to right, #000 50%, #0003 50%)' }}\n            >\n              <div\n                className=\"voice-input-waveform flex h-full w-[200%]\"\n                style={{ animationPlayState: isRecording ? 'running' : 'paused' }}\n              >\n                {[0, 1].map((copy) => (\n                  <div\n                    key={copy}\n                    className=\"flex h-full w-1/2 shrink-0 items-center justify-around\"\n                  >\n                    {waveform.map((height, index) => (\n                      <span\n                        className=\"h-full w-0.5 shrink-0 rounded-full bg-muted-foreground\"\n                        key={`${index}-${height}`}\n                        style={{ height: `${height}%` }}\n                      />\n                    ))}\n                  </div>\n                ))}\n              </div>\n            </div>\n            <div className=\"absolute inset-y-0 left-1/2 w-px bg-destructive\">\n              <span className=\"absolute bottom-0 left-1/2 size-0 -translate-x-1/2 border-x-[5px] border-b-[6px] border-x-transparent border-b-destructive\" />\n            </div>\n          </div>\n        </div>\n        <div className=\"mt-3 flex h-8 items-center gap-2 px-1\">\n          <span className=\"relative flex size-2\" aria-hidden=\"true\">\n            {isRecording && (\n              <span className=\"absolute inline-flex size-full animate-ping rounded-full bg-destructive opacity-75 motion-reduce:animate-none\" />\n            )}\n            <span\n              className={cn(\n                'relative inline-flex size-2 rounded-full',\n                isRecording ? 'bg-destructive' : 'bg-muted-foreground',\n              )}\n            />\n          </span>\n          <span className=\"text-xs leading-5 tabular-nums\">{duration}</span>\n          <span className=\"sr-only\">\n            {isRecording ? 'Recording' : hasTake ? 'Recording paused' : 'Not recording'}\n          </span>\n          <AnimatePresence initial={false}>\n            {hasTake && (onCancel || onSend) && (\n              <motion.div\n                animate={{ opacity: 1, x: 0 }}\n                className=\"ml-auto flex items-center gap-1.5\"\n                exit={{ opacity: 0, x: 6 }}\n                initial={{ opacity: 0, x: 6 }}\n                transition={{ duration: 0.2, ease }}\n              >\n                {onCancel && (\n                  <Button\n                    className=\"h-8 rounded-full px-3 text-muted-foreground hover:text-muted-foreground\"\n                    onClick={onCancel}\n                    size=\"sm\"\n                    type=\"button\"\n                    variant=\"ghost\"\n                  >\n                    <TrashIcon aria-hidden=\"true\" data-icon=\"inline-start\" /> Discard\n                  </Button>\n                )}\n                {onSend && (\n                  <Button\n                    aria-label=\"Send recording\"\n                    className={cn('size-8', primaryButton)}\n                    onClick={onSend}\n                    size=\"icon\"\n                    type=\"button\"\n                    variant=\"default\"\n                  >\n                    <ArrowUpIcon aria-hidden=\"true\" className=\"size-4\" />\n                  </Button>\n                )}\n              </motion.div>\n            )}\n          </AnimatePresence>\n        </div>\n      </div>\n    </MotionConfig>\n  )\n}\n\nexport { VoiceInput }\nexport type { VoiceInputProps }\n",
      "type": "registry:component",
      "target": "@components/ai/voice-input.tsx"
    }
  ],
  "css": {
    ".voice-input-waveform": {
      "animation": "voice-input-scroll 8s linear infinite"
    },
    "@keyframes voice-input-scroll": {
      "from": {
        "transform": "translateX(0)"
      },
      "to": {
        "transform": "translateX(-50%)"
      }
    },
    "@media (prefers-reduced-motion: reduce)": {
      ".voice-input-waveform": {
        "animation": "none"
      }
    }
  },
  "type": "registry:component"
}