{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "live-waveform",
  "title": "Live Waveform",
  "description": "A customizable live audio waveform visualizer using the Web Audio API.",
  "dependencies": [
    "react"
  ],
  "files": [
    {
      "path": "registry/gammaui/live-waveform.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useRef, type HTMLAttributes } from \"react\"\n\nexport type LiveWaveformProps = HTMLAttributes<HTMLDivElement> & {\n  active?: boolean\n  processing?: boolean\n  deviceId?: string\n  barWidth?: number\n  barGap?: number\n  barRadius?: number\n  barColor?: string\n  fadeEdges?: boolean\n  fadeWidth?: number\n  height?: string | number\n  sensitivity?: number\n  smoothingTimeConstant?: number\n  fftSize?: number\n  historySize?: number\n  updateRate?: number\n  mode?: \"scrolling\" | \"static\"\n  onError?: (error: Error) => void\n  onStreamReady?: (stream: MediaStream) => void\n  onStreamEnd?: () => void\n}\n\nexport function LiveWaveform({\n  active = false,\n  processing = false,\n  deviceId,\n  barWidth = 3,\n  barGap = 1,\n  barRadius = 1.5,\n  barColor,\n  fadeEdges = true,\n  fadeWidth = 24,\n  height = 64,\n  sensitivity = 1,\n  smoothingTimeConstant = 0.8,\n  fftSize = 256,\n  historySize = 60,\n  updateRate = 30,\n  mode = \"static\",\n  onError,\n  onStreamReady,\n  onStreamEnd,\n  className,\n  ...props\n}: LiveWaveformProps) {\n  const canvasRef = useRef<HTMLCanvasElement>(null)\n  const containerRef = useRef<HTMLDivElement>(null)\n  const historyRef = useRef<number[]>([])\n  const analyserRef = useRef<AnalyserNode | null>(null)\n  const audioContextRef = useRef<AudioContext | null>(null)\n  const streamRef = useRef<MediaStream | null>(null)\n  const animationRef = useRef<number>(0)\n  const lastUpdateRef = useRef<number>(0)\n  const processingAnimationRef = useRef<number | null>(null)\n  const lastActiveDataRef = useRef<number[]>([])\n  const transitionProgressRef = useRef(0)\n  const staticBarsRef = useRef<number[]>([])\n  const needsRedrawRef = useRef(true)\n  const gradientCacheRef = useRef<CanvasGradient | null>(null)\n  const lastWidthRef = useRef(0)\n  const currentWidthRef = useRef(0) // Track current width\n\n  const heightStyle = typeof height === \"number\" ? `${height}px` : height\n\n  // Handle canvas resizing\n  useEffect(() => {\n    const canvas = canvasRef.current\n    const container = containerRef.current\n    if (!canvas || !container) return\n\n    const updateCanvasSize = () => {\n      const rect = container.getBoundingClientRect()\n      const dpr = window.devicePixelRatio || 1\n\n      // Store the current width for use in animation loop\n      currentWidthRef.current = rect.width\n\n      canvas.width = rect.width * dpr\n      canvas.height = rect.height * dpr\n      canvas.style.width = `${containerRef.current?.offsetWidth}px`\n      canvas.style.height = `${rect.height}px`\n\n      const ctx = canvas.getContext(\"2d\")\n      if (ctx) {\n        ctx.scale(dpr, dpr)\n      }\n\n      gradientCacheRef.current = null\n      lastWidthRef.current = rect.width\n      needsRedrawRef.current = true\n    }\n\n    // Initial size\n    updateCanvasSize()\n\n    const resizeObserver = new ResizeObserver(() => {\n      updateCanvasSize()\n    })\n\n    resizeObserver.observe(container)\n    return () => resizeObserver.disconnect()\n  }, [])\n\n  useEffect(() => {\n    if (processing && !active) {\n      let time = 0\n      transitionProgressRef.current = 0\n\n      const animateProcessing = () => {\n        time += 0.03\n        transitionProgressRef.current = Math.min(\n          1,\n          transitionProgressRef.current + 0.02\n        )\n\n        const processingData = []\n        // Use currentWidthRef instead of getBoundingClientRect\n        const barCount = Math.floor(\n          currentWidthRef.current / (barWidth + barGap)\n        )\n\n        if (mode === \"static\") {\n          const halfCount = Math.floor(barCount / 2)\n\n          for (let i = 0; i < barCount; i++) {\n            const normalizedPosition = (i - halfCount) / halfCount\n            const centerWeight = 1 - Math.abs(normalizedPosition) * 0.4\n\n            const wave1 = Math.sin(time * 1.5 + normalizedPosition * 3) * 0.25\n            const wave2 = Math.sin(time * 0.8 - normalizedPosition * 2) * 0.2\n            const wave3 = Math.cos(time * 2 + normalizedPosition) * 0.15\n            const combinedWave = wave1 + wave2 + wave3\n            const processingValue = (0.2 + combinedWave) * centerWeight\n\n            let finalValue = processingValue\n            if (\n              lastActiveDataRef.current.length > 0 &&\n              transitionProgressRef.current < 1\n            ) {\n              const lastDataIndex = Math.min(\n                i,\n                lastActiveDataRef.current.length - 1\n              )\n              const lastValue = lastActiveDataRef.current[lastDataIndex] || 0\n              finalValue =\n                lastValue * (1 - transitionProgressRef.current) +\n                processingValue * transitionProgressRef.current\n            }\n\n            processingData.push(Math.max(0.05, Math.min(1, finalValue)))\n          }\n        } else {\n          for (let i = 0; i < barCount; i++) {\n            const normalizedPosition = (i - barCount / 2) / (barCount / 2)\n            const centerWeight = 1 - Math.abs(normalizedPosition) * 0.4\n\n            const wave1 = Math.sin(time * 1.5 + i * 0.15) * 0.25\n            const wave2 = Math.sin(time * 0.8 - i * 0.1) * 0.2\n            const wave3 = Math.cos(time * 2 + i * 0.05) * 0.15\n            const combinedWave = wave1 + wave2 + wave3\n            const processingValue = (0.2 + combinedWave) * centerWeight\n\n            let finalValue = processingValue\n            if (\n              lastActiveDataRef.current.length > 0 &&\n              transitionProgressRef.current < 1\n            ) {\n              const lastDataIndex = Math.floor(\n                (i / barCount) * lastActiveDataRef.current.length\n              )\n              const lastValue = lastActiveDataRef.current[lastDataIndex] || 0\n              finalValue =\n                lastValue * (1 - transitionProgressRef.current) +\n                processingValue * transitionProgressRef.current\n            }\n\n            processingData.push(Math.max(0.05, Math.min(1, finalValue)))\n          }\n        }\n\n        if (mode === \"static\") {\n          staticBarsRef.current = processingData\n        } else {\n          historyRef.current = processingData\n        }\n\n        needsRedrawRef.current = true\n        processingAnimationRef.current =\n          requestAnimationFrame(animateProcessing)\n      }\n\n      animateProcessing()\n\n      return () => {\n        if (processingAnimationRef.current) {\n          cancelAnimationFrame(processingAnimationRef.current)\n        }\n      }\n    } else if (!active && !processing) {\n      const hasData =\n        mode === \"static\"\n          ? staticBarsRef.current.length > 0\n          : historyRef.current.length > 0\n\n      if (hasData) {\n        let fadeProgress = 0\n        const fadeToIdle = () => {\n          fadeProgress += 0.03\n          if (fadeProgress < 1) {\n            if (mode === \"static\") {\n              staticBarsRef.current = staticBarsRef.current.map(\n                (value) => value * (1 - fadeProgress)\n              )\n            } else {\n              historyRef.current = historyRef.current.map(\n                (value) => value * (1 - fadeProgress)\n              )\n            }\n            needsRedrawRef.current = true\n            requestAnimationFrame(fadeToIdle)\n          } else {\n            if (mode === \"static\") {\n              staticBarsRef.current = []\n            } else {\n              historyRef.current = []\n            }\n          }\n        }\n        fadeToIdle()\n      }\n    }\n  }, [processing, active, barWidth, barGap, mode])\n\n  // Handle microphone setup and teardown\n  useEffect(() => {\n    if (!active) {\n      if (streamRef.current) {\n        streamRef.current.getTracks().forEach((track) => track.stop())\n        streamRef.current = null\n        onStreamEnd?.()\n      }\n      if (\n        audioContextRef.current &&\n        audioContextRef.current.state !== \"closed\"\n      ) {\n        audioContextRef.current.close()\n        audioContextRef.current = null\n      }\n      if (animationRef.current) {\n        cancelAnimationFrame(animationRef.current)\n        animationRef.current = 0\n      }\n      return\n    }\n\n    const setupMicrophone = async () => {\n      try {\n        const stream = await navigator.mediaDevices.getUserMedia({\n          audio: deviceId\n            ? {\n                deviceId: { exact: deviceId },\n                echoCancellation: true,\n                noiseSuppression: true,\n                autoGainControl: true,\n              }\n            : {\n                echoCancellation: true,\n                noiseSuppression: true,\n                autoGainControl: true,\n              },\n        })\n        streamRef.current = stream\n        onStreamReady?.(stream)\n\n        const AudioContextConstructor =\n          window.AudioContext || (window as any).webkitAudioContext\n        const audioContext = new AudioContextConstructor()\n        const analyser = audioContext.createAnalyser()\n        analyser.fftSize = fftSize\n        analyser.smoothingTimeConstant = smoothingTimeConstant\n\n        const source = audioContext.createMediaStreamSource(stream)\n        source.connect(analyser)\n\n        audioContextRef.current = audioContext\n        analyserRef.current = analyser\n\n        historyRef.current = []\n      } catch (error) {\n        onError?.(error as Error)\n      }\n    }\n\n    setupMicrophone()\n\n    return () => {\n      if (streamRef.current) {\n        streamRef.current.getTracks().forEach((track) => track.stop())\n        streamRef.current = null\n        onStreamEnd?.()\n      }\n      if (\n        audioContextRef.current &&\n        audioContextRef.current.state !== \"closed\"\n      ) {\n        audioContextRef.current.close()\n        audioContextRef.current = null\n      }\n      if (animationRef.current) {\n        cancelAnimationFrame(animationRef.current)\n        animationRef.current = 0\n      }\n    }\n  }, [\n    active,\n    deviceId,\n    fftSize,\n    smoothingTimeConstant,\n    onError,\n    onStreamReady,\n    onStreamEnd,\n  ])\n\n  // Animation loop\n  useEffect(() => {\n    const canvas = canvasRef.current\n    if (!canvas) return\n\n    const ctx = canvas.getContext(\"2d\")\n    if (!ctx) return\n\n    let rafId: number\n\n    const animate = (currentTime: number) => {\n      // Use currentWidthRef for consistent width throughout render\n      const width = currentWidthRef.current\n      const rect = canvas.getBoundingClientRect()\n\n      // Update audio data if active\n      if (active && currentTime - lastUpdateRef.current > updateRate) {\n        lastUpdateRef.current = currentTime\n\n        if (analyserRef.current) {\n          const dataArray = new Uint8Array(\n            analyserRef.current.frequencyBinCount\n          )\n          analyserRef.current.getByteFrequencyData(dataArray)\n\n          if (mode === \"static\") {\n            const startFreq = Math.floor(dataArray.length * 0.05)\n            const endFreq = Math.floor(dataArray.length * 0.4)\n            const relevantData = dataArray.slice(startFreq, endFreq)\n\n            const barCount = Math.floor(width / (barWidth + barGap))\n            const halfCount = Math.floor(barCount / 2)\n            const newBars: number[] = []\n\n            for (let i = halfCount - 1; i >= 0; i--) {\n              const dataIndex = Math.floor(\n                (i / halfCount) * relevantData.length\n              )\n              const value = Math.min(\n                1,\n                (relevantData[dataIndex] / 255) * sensitivity\n              )\n              newBars.push(Math.max(0.05, value))\n            }\n\n            for (let i = 0; i < halfCount; i++) {\n              const dataIndex = Math.floor(\n                (i / halfCount) * relevantData.length\n              )\n              const value = Math.min(\n                1,\n                (relevantData[dataIndex] / 255) * sensitivity\n              )\n              newBars.push(Math.max(0.05, value))\n            }\n\n            staticBarsRef.current = newBars\n            lastActiveDataRef.current = newBars\n          } else {\n            let sum = 0\n            const startFreq = Math.floor(dataArray.length * 0.05)\n            const endFreq = Math.floor(dataArray.length * 0.4)\n            const relevantData = dataArray.slice(startFreq, endFreq)\n\n            for (let i = 0; i < relevantData.length; i++) {\n              sum += relevantData[i]\n            }\n            const average = (sum / relevantData.length / 255) * sensitivity\n\n            historyRef.current.push(Math.min(1, Math.max(0.05, average)))\n            lastActiveDataRef.current = [...historyRef.current]\n\n            if (historyRef.current.length > historySize) {\n              historyRef.current.shift()\n            }\n          }\n          needsRedrawRef.current = true\n        }\n      }\n\n      if (!needsRedrawRef.current && !active) {\n        rafId = requestAnimationFrame(animate)\n        return\n      }\n\n      needsRedrawRef.current = active\n      ctx.clearRect(0, 0, width, rect.height)\n\n      const computedBarColor =\n        barColor ||\n        (() => {\n          const style = getComputedStyle(canvas)\n          const color = style.color\n          return color || \"#000\"\n        })()\n\n      const step = barWidth + barGap\n      const barCount = Math.floor(width / step)\n      const centerY = rect.height / 2\n\n      if (mode === \"static\") {\n        const dataToRender = processing\n          ? staticBarsRef.current\n          : active\n            ? staticBarsRef.current\n            : staticBarsRef.current.length > 0\n              ? staticBarsRef.current\n              : []\n\n        for (let i = 0; i < barCount && i < dataToRender.length; i++) {\n          const value = dataToRender[i] || 0.1\n          const x = i * step\n          const barHeight = Math.max(4, value * rect.height * 0.8)\n          const y = centerY - barHeight / 2\n\n          ctx.fillStyle = computedBarColor\n          ctx.globalAlpha = 0.4 + value * 0.6\n\n          if (barRadius > 0) {\n            ctx.beginPath()\n            ctx.roundRect(x, y, barWidth, barHeight, barRadius)\n            ctx.fill()\n          } else {\n            ctx.fillRect(x, y, barWidth, barHeight)\n          }\n        }\n      } else {\n        for (let i = 0; i < barCount && i < historyRef.current.length; i++) {\n          const dataIndex = historyRef.current.length - 1 - i\n          const value = historyRef.current[dataIndex] || 0.1\n          const x = width - (i + 1) * step\n          const barHeight = Math.max(4, value * rect.height * 0.8)\n          const y = centerY - barHeight / 2\n\n          ctx.fillStyle = computedBarColor\n          ctx.globalAlpha = 0.4 + value * 0.6\n\n          if (barRadius > 0) {\n            ctx.beginPath()\n            ctx.roundRect(x, y, barWidth, barHeight, barRadius)\n            ctx.fill()\n          } else {\n            ctx.fillRect(x, y, barWidth, barHeight)\n          }\n        }\n      }\n\n      if (fadeEdges && fadeWidth > 0 && width > 0) {\n        if (!gradientCacheRef.current || lastWidthRef.current !== width) {\n          const gradient = ctx.createLinearGradient(0, 0, width, 0)\n          const fadePercent = Math.min(0.3, fadeWidth / width)\n\n          gradient.addColorStop(0, \"rgba(255,255,255,1)\")\n          gradient.addColorStop(fadePercent, \"rgba(255,255,255,0)\")\n          gradient.addColorStop(1 - fadePercent, \"rgba(255,255,255,0)\")\n          gradient.addColorStop(1, \"rgba(255,255,255,1)\")\n\n          gradientCacheRef.current = gradient\n          lastWidthRef.current = width\n        }\n\n        ctx.globalCompositeOperation = \"destination-out\"\n        ctx.fillStyle = gradientCacheRef.current\n        ctx.fillRect(0, 0, width, rect.height)\n        ctx.globalCompositeOperation = \"source-over\"\n      }\n\n      ctx.globalAlpha = 1\n\n      rafId = requestAnimationFrame(animate)\n    }\n\n    rafId = requestAnimationFrame(animate)\n\n    return () => {\n      if (rafId) {\n        cancelAnimationFrame(rafId)\n      }\n    }\n  }, [\n    active,\n    processing,\n    sensitivity,\n    updateRate,\n    historySize,\n    barWidth,\n    barGap,\n    barRadius,\n    barColor,\n    fadeEdges,\n    fadeWidth,\n    mode,\n  ])\n\n  return (\n    <div\n      className={`relative h-full w-full ${className || \"\"}`}\n      ref={containerRef}\n      style={{ height: heightStyle }}\n      aria-label={\n        active\n          ? \"Live audio waveform\"\n          : processing\n            ? \"Processing audio\"\n            : \"Audio waveform idle\"\n      }\n      role=\"img\"\n      {...props}\n    >\n      {!active && !processing && (\n        <div className=\"border-muted-foreground/20 absolute top-1/2 right-0 left-0 -translate-y-1/2 border-t-2 border-dotted\" />\n      )}\n      <canvas\n        className=\"block h-full w-full\"\n        ref={canvasRef}\n        aria-hidden=\"true\"\n      />\n    </div>\n  )\n}\n\nexport default LiveWaveform\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}