{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "voice-wave",
  "title": "Voice Wave",
  "description": "Bars that rise with the microphone level so the user can see they are being heard, and see when they are not.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/miseui/ui/voice-wave.tsx",
      "content": "import type * as React from \"react\"\nimport { motion, useReducedMotion } from \"motion/react\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\nconst DEFAULT_BARS = 5\n/** Resting height, as a fraction of the full bar. Visible when idle, so the control never looks empty. */\nconst IDLE_SCALE = 0.25\n/** Outer bars still reach this share of the centre bar's height, so the wave stays readable at low levels. */\nconst EDGE_WEIGHT = 0.35\n/** Matches --duration-fast: quick enough to track speech, slow enough not to flicker. */\nconst LEVEL_TRANSITION_SECONDS = 0.15\nconst LABEL_LISTENING = \"Listening\"\nconst LABEL_OFF = \"Microphone off\"\n\ninterface VoiceWaveProps extends React.ComponentProps<\"div\"> {\n  /** Current input level, 0..1. Values outside the range are clamped. */\n  level?: number\n  /** Whether the microphone is listening. When false every bar rests at its idle height. */\n  active?: boolean\n  bars?: number\n  /** Overrides the accessible name (\"Listening\" / \"Microphone off\"). */\n  label?: string\n}\n\nfunction clampLevel(level: number) {\n  if (!Number.isFinite(level)) return 0\n  return Math.min(1, Math.max(0, level))\n}\n\n/** 1 at the centre bar, falling on a cosine towards EDGE_WEIGHT at the outer bars. */\nfunction barWeight(index: number, bars: number) {\n  const centre = (bars - 1) / 2\n  const distance = centre === 0 ? 0 : Math.abs(index - centre) / centre\n  return EDGE_WEIGHT + (1 - EDGE_WEIGHT) * Math.cos((distance * Math.PI) / 2)\n}\n\nfunction barScale(index: number, bars: number, level: number, active: boolean) {\n  if (!active) return IDLE_SCALE\n  return IDLE_SCALE + (1 - IDLE_SCALE) * level * barWeight(index, bars)\n}\n\nfunction VoiceWave({\n  level = 0,\n  active = false,\n  bars = DEFAULT_BARS,\n  label,\n  className,\n  ...props\n}: VoiceWaveProps) {\n  const reduceMotion = useReducedMotion()\n  const clampedLevel = clampLevel(level)\n  const barCount = Math.max(1, Math.floor(bars))\n\n  return (\n    <div\n      role=\"img\"\n      aria-label={label ?? (active ? LABEL_LISTENING : LABEL_OFF)}\n      data-slot=\"voice-wave\"\n      data-active={active ? \"true\" : \"false\"}\n      data-level={clampedLevel}\n      className={cn(\"inline-flex h-6 items-center gap-1\", className)}\n      {...props}\n    >\n      {Array.from({ length: barCount }, (_, index) => (\n        <motion.span\n          key={index}\n          data-testid=\"voice-bar\"\n          className={cn(\"h-full w-1 origin-center rounded-full\", active ? \"bg-mise\" : \"bg-muted-foreground\")}\n          initial={false}\n          animate={{ scaleY: barScale(index, barCount, clampedLevel, active) }}\n          transition={reduceMotion ? { duration: 0 } : { duration: LEVEL_TRANSITION_SECONDS, ease: \"easeOut\" }}\n        />\n      ))}\n    </div>\n  )\n}\n\nexport { VoiceWave, type VoiceWaveProps }\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "ai"
  ],
  "type": "registry:ui"
}