{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ai-voice-input",
  "title": "AI voice input",
  "description": "AI voice input is a production-ready ai component from BJORK UI, packaged for shadcn registries.",
  "dependencies": [
    "clsx",
    "framer-motion",
    "lucide-react",
    "tailwind-merge"
  ],
  "files": [
    {
      "path": "components/bjork-ui/ai/ai-voice-input.tsx",
      "content": "\"use client\";\n\nimport React, { useState, useEffect } from \"react\";\nimport { motion, AnimatePresence, useReducedMotion } from \"framer-motion\";\nimport { Send, AudioLines, Square } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\ninterface AIVoiceInputProps {\n  placeholder?: string;\n  onSend?: (message: string) => void;\n  onVoiceStart?: () => void;\n  onVoiceStop?: (duration: number) => void;\n  enableAnimations?: boolean;\n  className?: string;\n  disabled?: boolean;\n  \n  // Voice settings\n  visualizerBars?: number;\n  demoMode?: boolean;\n  demoInterval?: number;\n}\n\nexport function AIVoiceInput({\n  placeholder = \"Send message...\",\n  onSend,\n  onVoiceStart,\n  onVoiceStop,\n  enableAnimations = true,\n  className,\n  disabled = false,\n  demoMode = false,\n  demoInterval = 3000,\n}: AIVoiceInputProps) {\n  const [message, setMessage] = useState(\"\");\n  const [isVoiceActive, setIsVoiceActive] = useState(false);\n  const [voiceTime, setVoiceTime] = useState(0);\n  const [isClient, setIsClient] = useState(false);\n  const [isDemo, setIsDemo] = useState(demoMode);\n\n  const shouldReduceMotion = useReducedMotion();\n  const shouldAnimate = enableAnimations && !shouldReduceMotion;\n\n  useEffect(() => {\n    setIsClient(true);\n  }, []);\n\n  useEffect(() => {\n    let intervalId: NodeJS.Timeout | undefined;\n\n    if (isVoiceActive) {\n      onVoiceStart?.();\n      intervalId = setInterval(() => {\n        setVoiceTime((t) => t + 1);\n      }, 1000);\n    } else {\n      onVoiceStop?.(voiceTime);\n      setVoiceTime(0);\n    }\n\n    return () => {\n      if (intervalId) clearInterval(intervalId);\n    };\n  }, [isVoiceActive, voiceTime, onVoiceStart, onVoiceStop]);\n\n  // Demo mode effect\n  useEffect(() => {\n    if (!isDemo) return;\n\n    let timeoutId: NodeJS.Timeout;\n    const runAnimation = () => {\n      setIsVoiceActive(true);\n      timeoutId = setTimeout(() => {\n        setIsVoiceActive(false);\n        timeoutId = setTimeout(runAnimation, 1000);\n      }, demoInterval);\n    };\n\n    const initialTimeout = setTimeout(runAnimation, 100);\n    return () => {\n      clearTimeout(timeoutId);\n      clearTimeout(initialTimeout);\n    };\n  }, [isDemo, demoInterval]);\n\n  const formatTime = (seconds: number) => {\n    const mins = Math.floor(seconds / 60);\n    const secs = seconds % 60;\n    return `${mins.toString().padStart(2, \"0\")}:${secs.toString().padStart(2, \"0\")}`;\n  };\n\n  const handleSubmit = (e: React.SyntheticEvent) => {\n    e.preventDefault();\n    if (message.trim() && onSend && !disabled) {\n      onSend(message.trim());\n      setMessage(\"\");\n    }\n  };\n\n  const handleKeyDown = (e: React.KeyboardEvent) => {\n    if (e.key === \"Enter\" && !e.shiftKey) {\n      e.preventDefault();\n      handleSubmit(e);\n    }\n  };\n\n  const handleVoiceClick = () => {\n    if (isDemo) {\n      setIsDemo(false);\n      setIsVoiceActive(false);\n    } else {\n      setIsVoiceActive(prev => !prev);\n    }\n  };\n\n  return (\n    <motion.div\n      className={cn(\n        \"relative mx-auto w-full overflow-hidden rounded-[20px]\",\n        \"border border-[color:var(--bjork-border)] bg-[var(--bjork-field)] text-[color:var(--bjork-text)] shadow-[var(--bjork-shadow-surface)] backdrop-blur-md\",\n        \"transition-[background-color,border-color,box-shadow,opacity] duration-200 ease-out\",\n        \"focus-within:border-[color:var(--bjork-border-strong)] focus-within:ring-2 focus-within:ring-[color:var(--bjork-border-strong)] focus-within:ring-offset-2 focus-within:ring-offset-[var(--bjork-ring-offset)]\",\n        disabled && \"opacity-50\",\n        className\n      )}\n      initial={shouldAnimate ? { opacity: 0, y: 8 } : {}}\n      animate={{ opacity: 1, y: 0 }}\n      transition={{\n        type: \"spring\",\n        stiffness: 300,\n        damping: 30,\n        mass: 0.8,\n      }}\n    >\n      <div className=\"relative p-4\">\n        <AnimatePresence>\n          {isVoiceActive && (\n            <motion.div\n              initial={{ opacity: 0 }}\n              animate={{ opacity: 1 }}\n              exit={{ opacity: 0 }}\n              transition={{ duration: 0.2 }}\n              className=\"absolute inset-0 z-10 flex items-center justify-center rounded-[18px] border border-[color:var(--bjork-border)] bg-[var(--bjork-menu)] shadow-[var(--bjork-shadow-menu)] backdrop-blur-xl\"\n            >\n              <div className=\"flex flex-col items-center gap-3\">\n                <button\n                  type=\"button\"\n                  aria-label=\"Stop voice input\"\n                  onClick={() => setIsVoiceActive(false)}\n                  className=\"absolute right-3 top-3 flex size-8 items-center justify-center rounded-[11px] border border-[color:var(--bjork-border)] bg-[var(--bjork-field-inset)] text-[#ec5c13] shadow-[var(--bjork-shadow-inset)] transition-[background-color,border-color,color,transform] duration-150 ease-out hover:border-[color:var(--bjork-border-strong)] hover:bg-[var(--bjork-panel)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#ec5c13]/35 focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bjork-ring-offset)]\"\n                >\n                  <Square className=\"size-3\" />\n                </button>\n\n                <span className=\"font-mono text-sm text-[color:var(--bjork-text-medium)]\">\n                  {formatTime(voiceTime)}\n                </span>\n\n                <div className=\"flex h-9 w-48 items-center justify-center gap-0.5\">\n                  {[...Array(24)].map((_, i) => (\n                    <div\n                      key={i}\n                      className={cn(\n                        \"w-0.5 rounded-full transition-all duration-300\",\n                        isVoiceActive\n                          ? \"animate-pulse bg-[var(--bjork-accent)] opacity-80\"\n                          : \"h-1 bg-[var(--bjork-text-faint)] opacity-45\"\n                      )}\n                      style={\n                        isVoiceActive && isClient\n                          ? {\n                              height: `${30 + Math.random() * 70}%`,\n                              animationDelay: `${i * 0.05}s`,\n                            }\n                          : undefined\n                      }\n                    />\n                  ))}\n                </div>\n\n                <p className=\"text-xs font-medium text-[color:var(--bjork-text-soft)]\">\n                  Listening...\n                </p>\n              </div>\n            </motion.div>\n          )}\n        </AnimatePresence>\n\n        <div className=\"mb-3 flex items-start gap-3\">\n          <div className=\"relative flex-1\">\n            <textarea\n              aria-label=\"Message\"\n              value={message}\n              onChange={(e) => setMessage(e.target.value)}\n              onKeyDown={handleKeyDown}\n              placeholder={placeholder}\n              disabled={disabled || isVoiceActive}\n              rows={1}\n              className={cn(\n                \"w-full resize-none border-0 bg-transparent px-0 py-1\",\n                \"text-base leading-6 text-[color:var(--bjork-text)] placeholder:text-[color:var(--bjork-text-soft)]\",\n                \"outline-none focus:outline-none focus:ring-0\",\n                \"overflow-hidden selection:bg-[#ec5c13]/30 selection:text-[color:var(--bjork-text)]\",\n                \"transition-[color,opacity] duration-200\",\n                (disabled || isVoiceActive) && \"opacity-50 cursor-not-allowed\",\n                isVoiceActive && \"opacity-30\"\n              )}\n              style={{\n                minHeight: \"40px\",\n                maxHeight: \"120px\",\n                height: \"auto\",\n                outline: \"none !important\",\n                boxShadow: \"none !important\",\n              }}\n              onInput={(e) => {\n                const target = e.target as HTMLTextAreaElement;\n                target.style.height = \"auto\";\n                target.style.height = Math.min(target.scrollHeight, 120) + \"px\";\n              }}\n            />\n          </div>\n\n          <AnimatePresence>\n            {message.trim() && (\n              <motion.button\n                type=\"submit\"\n                onClick={handleSubmit}\n                disabled={disabled || !message.trim()}\n                initial={{ opacity: 0, scale: 0.8 }}\n                animate={{ opacity: 1, scale: 1 }}\n                exit={{ opacity: 0, scale: 0.8 }}\n                className={cn(\n                  \"mt-1 flex size-9 shrink-0 transform-gpu items-center justify-center rounded-[13px]\",\n                  \"border border-transparent bg-[var(--bjork-surface-active)] text-[color:var(--bjork-text-medium)] shadow-[var(--bjork-shadow-soft)]\",\n                  \"transition-[background-color,color,box-shadow,opacity] duration-150 ease-out hover:bg-[var(--bjork-surface-hover)] hover:text-[color:var(--bjork-text)]\",\n                  \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#ec5c13]/35 focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bjork-ring-offset)]\",\n                  disabled && \"opacity-50 cursor-not-allowed\"\n                )}\n                whileHover={shouldAnimate ? { scale: 1.1 } : {}}\n                whileTap={shouldAnimate ? { scale: 0.9 } : {}}\n                transition={{\n                  type: \"spring\",\n                  stiffness: 400,\n                  damping: 25,\n                }}\n              >\n                <Send className=\"size-4\" />\n              </motion.button>\n            )}\n          </AnimatePresence>\n        </div>\n\n        <div className=\"flex items-center justify-end\">\n          <motion.button\n            type=\"button\"\n            aria-pressed={isVoiceActive}\n            onClick={handleVoiceClick}\n            disabled={disabled}\n            className={cn(\n              \"inline-flex h-9 transform-gpu items-center gap-2 rounded-[13px] px-3 text-sm font-medium\",\n              \"border border-[color:var(--bjork-border)] bg-[var(--bjork-field-inset)] text-[color:var(--bjork-text-medium)] shadow-[var(--bjork-shadow-inset)]\",\n              \"transition-[background-color,border-color,color,box-shadow,opacity] duration-150 ease-out hover:border-[color:var(--bjork-border-strong)] hover:bg-[var(--bjork-panel)] hover:text-[color:var(--bjork-text)]\",\n              \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#ec5c13]/35 focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bjork-ring-offset)]\",\n              disabled && \"opacity-50 cursor-not-allowed\"\n            )}\n            whileHover={shouldAnimate ? { scale: 1.02 } : {}}\n            whileTap={shouldAnimate ? { scale: 0.98 } : {}}\n            transition={{\n              type: \"spring\",\n              stiffness: 400,\n              damping: 25,\n            }}\n          >\n            <AudioLines className=\"size-3.5\" aria-hidden=\"true\" />\n            <span>Voice</span>\n          </motion.button>\n        </div>\n      </div>\n    </motion.div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/bjork-ui/ai/ai-voice-input.tsx"
    },
    {
      "path": "lib/utils.ts",
      "content": "import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n",
      "type": "registry:lib",
      "target": "@lib/utils.ts"
    }
  ],
  "cssVars": {
    "light": {
      "bjork-bg": "#f7f3ea",
      "bjork-surface": "#fffcf6",
      "bjork-surface-muted": "rgba(250, 246, 237, 0.94)",
      "bjork-surface-hover": "#f5efe3",
      "bjork-surface-active": "#efe7d8",
      "bjork-panel": "#f8f2e7",
      "bjork-menu": "rgba(255, 252, 246, 0.98)",
      "bjork-field": "rgba(255, 252, 246, 0.96)",
      "bjork-field-muted": "rgba(248, 242, 231, 0.82)",
      "bjork-field-inset": "rgba(239, 231, 216, 0.72)",
      "bjork-error-bg": "#fff0e8",
      "bjork-text": "#171717",
      "bjork-text-strong": "rgba(23, 23, 23, 0.9)",
      "bjork-text-medium": "rgba(23, 23, 23, 0.72)",
      "bjork-text-muted": "rgba(23, 23, 23, 0.52)",
      "bjork-text-soft": "rgba(23, 23, 23, 0.36)",
      "bjork-text-faint": "rgba(23, 23, 23, 0.22)",
      "bjork-inverted-text": "#fff8f0",
      "bjork-border": "#eee6db",
      "bjork-border-muted": "#f5ede2",
      "bjork-border-strong": "#e1d7c8",
      "bjork-thumb": "#a49b8e",
      "bjork-track": "#ded6ca",
      "bjork-accent": "#ec7d43",
      "bjork-accent-hover": "#f0935f",
      "bjork-accent-soft": "rgba(236, 125, 67, 0.1)",
      "bjork-accent-muted": "rgba(236, 125, 67, 0.2)",
      "bjork-accent-badge": "rgba(236, 125, 67, 0.16)",
      "bjork-accent-badge-foreground": "#8d421d",
      "bjork-accent-foreground": "#3f2112",
      "bjork-shadow-surface": "inset 0 7px 14px rgba(88, 72, 49, 0.045), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.92), inset 1px 0 0 rgba(88, 72, 49, 0.026), inset -1px 0 0 rgba(255, 255, 255, 0.68), 0 14px 22px -9px rgba(66, 52, 33, 0.11)",
      "bjork-shadow-soft": "inset 0 1px 0 rgba(88, 72, 49, 0.045), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.86)",
      "bjork-shadow-panel": "inset 0 1px 0 rgba(88, 72, 49, 0.04), inset 1px 0 0 rgba(88, 72, 49, 0.024), inset -1px 0 0 rgba(255, 255, 255, 0.64), 0 18px 38px -30px rgba(66, 52, 33, 0.2)",
      "bjork-shadow-menu": "inset 0 1px 0 rgba(88, 72, 49, 0.04), inset 0 12px 24px rgba(88, 72, 49, 0.022), inset 1px 0 0 rgba(88, 72, 49, 0.024), inset -1px 0 0 rgba(255, 255, 255, 0.62), 0 22px 44px -26px rgba(66, 52, 33, 0.22)",
      "bjork-shadow-inset": "inset 0 1px 10px rgba(88, 72, 49, 0.12), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.72)",
      "bjork-ring-offset": "#f7f3ea"
    },
    "dark": {
      "bjork-bg": "#111111",
      "bjork-surface": "#121212",
      "bjork-surface-muted": "rgba(22, 22, 22, 0.92)",
      "bjork-surface-hover": "#161616",
      "bjork-surface-active": "#202020",
      "bjork-panel": "#0d0d0d",
      "bjork-menu": "rgba(18, 18, 18, 0.98)",
      "bjork-field": "rgba(18, 18, 18, 0.95)",
      "bjork-field-muted": "rgba(18, 18, 18, 0.55)",
      "bjork-field-inset": "rgba(9, 9, 9, 0.75)",
      "bjork-error-bg": "#130d0a",
      "bjork-text": "#ededed",
      "bjork-text-strong": "rgba(237, 237, 237, 0.9)",
      "bjork-text-medium": "rgba(237, 237, 237, 0.72)",
      "bjork-text-muted": "rgba(237, 237, 237, 0.52)",
      "bjork-text-soft": "rgba(237, 237, 237, 0.36)",
      "bjork-text-faint": "rgba(237, 237, 237, 0.22)",
      "bjork-inverted-text": "#080808",
      "bjork-border": "#232323",
      "bjork-border-muted": "#1c1c1c",
      "bjork-border-strong": "#343434",
      "bjork-thumb": "#626262",
      "bjork-track": "#090909",
      "bjork-accent": "#ec5c13",
      "bjork-accent-hover": "#f07832",
      "bjork-accent-soft": "rgba(236, 92, 19, 0.12)",
      "bjork-accent-muted": "rgba(236, 92, 19, 0.24)",
      "bjork-accent-badge": "#ec5c13",
      "bjork-accent-badge-foreground": "#fff2ea",
      "bjork-accent-foreground": "#fff2ea",
      "bjork-shadow-surface": "inset 0 7px 14px rgba(255, 255, 255, 0.03), inset 0 0.5px 0.5px rgba(255, 255, 255, 0.06), 0 14px 20px -6px rgba(0, 0, 0, 0.45)",
      "bjork-shadow-soft": "inset 0 1px 0 rgba(255, 255, 255, 0.045)",
      "bjork-shadow-panel": "inset 0 1px 0 rgba(255, 255, 255, 0.035), 0 18px 36px -28px rgba(0, 0, 0, 0.9)",
      "bjork-shadow-menu": "inset 0 1px 0 rgba(255, 255, 255, 0.055), inset 0 12px 24px rgba(255, 255, 255, 0.018), inset 0 -18px 26px rgba(0, 0, 0, 0.24), 0 22px 42px -22px rgba(0, 0, 0, 0.9)",
      "bjork-shadow-inset": "inset 0 1px 10px rgba(0, 0, 0, 0.45)",
      "bjork-ring-offset": "#080808"
    }
  },
  "css": {
    ".bjork-range": {
      "box-shadow": "inset 0 1px 1px rgba(255, 255, 255, 0.04), 0 0 14px rgba(189, 69, 20, 0.06)"
    },
    ".bjork-range::-webkit-slider-thumb": {
      "appearance": "none",
      "width": "16px",
      "height": "8px",
      "border": "0",
      "border-radius": "999px",
      "background": "var(--bjork-thumb)",
      "box-shadow": "inset 0 1px 0 rgba(255, 255, 255, 0.18), 0 0 0 1px rgba(0, 0, 0, 0.42)"
    },
    ".bjork-range::-moz-range-thumb": {
      "width": "16px",
      "height": "8px",
      "border": "0",
      "border-radius": "999px",
      "background": "var(--bjork-thumb)",
      "box-shadow": "inset 0 1px 0 rgba(255, 255, 255, 0.18), 0 0 0 1px rgba(0, 0, 0, 0.42)"
    },
    ".bjork-layered-button-accent, .bjork-layered-button-raised": {
      "position": "relative",
      "isolation": "isolate",
      "appearance": "none",
      "overflow": "hidden",
      "border": "0",
      "border-radius": "13px",
      "box-shadow": "inset 0 1px 0 #ffffff38, 0 4px 6px -1px #0000001a, 0 2px 4px -2px #0000001a",
      "transition": "filter 150ms ease-out, box-shadow 150ms ease-out"
    },
    ".bjork-layered-button-accent::after, .bjork-layered-button-raised::after": {
      "position": "absolute",
      "inset": "0",
      "pointer-events": "none",
      "content": "\"\"",
      "border": "1.5px solid transparent",
      "border-radius": "inherit",
      "background": "linear-gradient(#ffffffb8, #0000003d 41% 75%, #ffffff47) border-box",
      "mix-blend-mode": "overlay",
      "-webkit-mask-image": "linear-gradient(#000, #000), linear-gradient(#000, #000)",
      "mask-image": "linear-gradient(#000, #000), linear-gradient(#000, #000)",
      "-webkit-mask-clip": "padding-box, border-box",
      "mask-clip": "padding-box, border-box",
      "-webkit-mask-origin": "padding-box, border-box",
      "mask-origin": "padding-box, border-box",
      "-webkit-mask-composite": "xor",
      "mask-composite": "exclude"
    },
    ".bjork-layered-button-accent": {
      "color": "oklch(100% 0 0)",
      "text-shadow": "0 -1px #00000040",
      "background": "radial-gradient(ellipse at -20px top, rgba(255, 255, 255, 0.14), transparent), linear-gradient(180deg, #ec5c13 0%, #ec5c13 62%, color-mix(in oklab, #ec5c13 84%, black 16%) 100%)"
    },
    ".bjork-layered-button-raised": {
      "color": "oklch(100% 0 0)",
      "text-shadow": "0 -1px #00000026",
      "background": "radial-gradient(ellipse at -20px top, #ffffff38, #fff0), linear-gradient(180deg, oklch(0.1881 0.006 265), oklch(0.5493 0.0081 265))"
    },
    ".dark .bjork-layered-button-raised": {
      "color": "oklch(0.2574 0.0056 265)",
      "text-shadow": "0 -1px #00000026",
      "background": "radial-gradient(ellipse at -20px top, #ffffff38, #fff0), linear-gradient(180deg, color-mix(in oklab, oklch(0.9674 0.0013 265) 80%, oklch(0.2574 0.0056 265) 20%), color-mix(in oklab, oklch(0.669 0.0107 265) 80%, oklch(0.2574 0.0056 265) 20%))"
    },
    "@media (hover: hover)": {
      ".bjork-layered-button-accent:hover": {
        "filter": "brightness(1.08) saturate(1.04)"
      },
      ".bjork-layered-button-raised:hover": {
        "filter": "brightness(1.08) contrast(0.9)"
      }
    }
  },
  "meta": {
    "collection": "AI",
    "registryUrl": "https://ui.isaiahbjork.com/ai-voice-input.json",
    "installCommand": "npx shadcn@latest add https://ui.isaiahbjork.com/ai-voice-input.json"
  },
  "type": "registry:component"
}
