{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "live-waveform", "type": "registry:ui", "title": "Live Waveform", "description": "Canvas-based real-time waveform visualizer with processing animation, microphone input, and stop controls.", "dependencies": [ "lucide-react" ], "files": [ { "path": "registry/ruixenui/live-waveform.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Square } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface LiveWaveformProps {\n /** Enable real-time microphone input visualization */\n active?: boolean;\n /** Show animated processing wave pattern */\n processing?: boolean;\n /** Bar width in pixels */\n barWidth?: number;\n /** Gap between bars in pixels */\n barGap?: number;\n /** Bar corner radius */\n barRadius?: number;\n /** Bar color — inherits currentColor when unset */\n barColor?: string;\n /** Waveform height in pixels */\n height?: number;\n /** Apply gradient fade at edges */\n fadeEdges?: boolean;\n /** Fade gradient width in pixels */\n fadeWidth?: number;\n /** Microphone sensitivity multiplier */\n sensitivity?: number;\n /** Callback when stop button is pressed */\n onStop?: () => void;\n /** Additional CSS classes */\n className?: string;\n}\n\nfunction roundedBar(\n ctx: CanvasRenderingContext2D,\n x: number,\n y: number,\n w: number,\n h: number,\n r: number,\n) {\n const radius = Math.min(r, w / 2, h / 2);\n ctx.beginPath();\n ctx.moveTo(x + radius, y);\n ctx.lineTo(x + w - radius, y);\n ctx.arcTo(x + w, y, x + w, y + radius, radius);\n ctx.lineTo(x + w, y + h - radius);\n ctx.arcTo(x + w, y + h, x + w - radius, y + h, radius);\n ctx.lineTo(x + radius, y + h);\n ctx.arcTo(x, y + h, x, y + h - radius, radius);\n ctx.lineTo(x, y + radius);\n ctx.arcTo(x, y, x + radius, y, radius);\n ctx.closePath();\n ctx.fill();\n}\n\nexport default function LiveWaveform({\n active = false,\n processing = false,\n barWidth = 3,\n barGap = 2,\n barRadius = 1.5,\n barColor,\n height = 48,\n fadeEdges = true,\n fadeWidth = 24,\n sensitivity = 1,\n onStop,\n className,\n}: LiveWaveformProps) {\n const canvasRef = React.useRef(null);\n const wrapRef = React.useRef(null);\n const frameRef = React.useRef(0);\n const analyserRef = React.useRef(null);\n const streamRef = React.useRef(null);\n const audioCtxRef = React.useRef(null);\n const dataRef = React.useRef | null>(null);\n\n const color = React.useCallback(() => {\n if (barColor) return barColor;\n const el = canvasRef.current;\n if (el) return getComputedStyle(el).color;\n return \"#a1a1aa\";\n }, [barColor]);\n\n /* -------- canvas animation -------- */\n React.useEffect(() => {\n const cvs = canvasRef.current;\n const wrap = wrapRef.current;\n if (!cvs || !wrap) return;\n const ctx = cvs.getContext(\"2d\");\n if (!ctx) return;\n\n const dpr = window.devicePixelRatio || 1;\n\n const resize = () => {\n const width = wrap.offsetWidth;\n cvs.width = Math.round(width * dpr);\n cvs.height = Math.round(height * dpr);\n cvs.style.width = `${width}px`;\n cvs.style.height = `${height}px`;\n };\n resize();\n\n const ro = new ResizeObserver(resize);\n ro.observe(wrap);\n\n const t0 = performance.now();\n\n const paint = (now: number) => {\n const w = wrap.offsetWidth;\n const h = height;\n\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n ctx.clearRect(0, 0, w, h);\n ctx.fillStyle = color();\n\n const step = barWidth + barGap;\n const count = Math.ceil(w / step) + 1;\n const t = (now - t0) / 1000;\n const cy = h / 2;\n\n for (let i = 0; i < count; i++) {\n const x = i * step;\n const pos = i / count;\n let bh = 4;\n let alpha = 1;\n\n if (processing && !active) {\n /* layered sine waves → organic flowing animation */\n const s1 = Math.sin(t * 2.5 + pos * Math.PI * 4) * 0.38;\n const s2 = Math.sin(t * 1.7 + pos * Math.PI * 6 + 1.3) * 0.24;\n const s3 = Math.sin(t * 3.3 + pos * Math.PI * 2.5 + 0.8) * 0.14;\n const amp = (s1 + s2 + s3 + 0.76) / 1.52;\n bh = Math.max(4, amp * h * 0.82);\n } else if (active && analyserRef.current) {\n const an = analyserRef.current;\n if (\n !dataRef.current ||\n dataRef.current.length !== an.frequencyBinCount\n ) {\n dataRef.current = new Uint8Array(an.frequencyBinCount);\n }\n an.getByteFrequencyData(dataRef.current);\n const idx = Math.floor(pos * dataRef.current.length);\n const val = (dataRef.current[idx] / 255) * sensitivity;\n bh = Math.max(4, val * h * 0.85);\n } else {\n /* idle: tiny flat bars */\n alpha = 0.25;\n }\n\n if (fadeEdges) {\n const edge = Math.min(x, w - x - barWidth);\n if (edge < fadeWidth) alpha *= Math.max(0, edge / fadeWidth);\n }\n\n ctx.globalAlpha = alpha;\n roundedBar(ctx, x, cy - bh / 2, barWidth, bh, barRadius);\n }\n\n ctx.globalAlpha = 1;\n frameRef.current = requestAnimationFrame(paint);\n };\n\n frameRef.current = requestAnimationFrame(paint);\n\n return () => {\n cancelAnimationFrame(frameRef.current);\n ro.disconnect();\n };\n }, [\n active,\n processing,\n barWidth,\n barGap,\n barRadius,\n height,\n fadeEdges,\n fadeWidth,\n sensitivity,\n color,\n ]);\n\n /* -------- microphone -------- */\n React.useEffect(() => {\n if (!active) {\n streamRef.current?.getTracks().forEach((t) => t.stop());\n streamRef.current = null;\n audioCtxRef.current?.close();\n audioCtxRef.current = null;\n analyserRef.current = null;\n return;\n }\n\n let cancelled = false;\n\n (async () => {\n try {\n const stream = await navigator.mediaDevices.getUserMedia({\n audio: true,\n });\n if (cancelled) {\n stream.getTracks().forEach((t) => t.stop());\n return;\n }\n streamRef.current = stream;\n\n const actx = new AudioContext();\n audioCtxRef.current = actx;\n\n const src = actx.createMediaStreamSource(stream);\n const analyser = actx.createAnalyser();\n analyser.fftSize = 256;\n analyser.smoothingTimeConstant = 0.8;\n src.connect(analyser);\n analyserRef.current = analyser;\n } catch (e) {\n console.error(\"Microphone access failed:\", e);\n }\n })();\n\n return () => {\n cancelled = true;\n streamRef.current?.getTracks().forEach((t) => t.stop());\n streamRef.current = null;\n audioCtxRef.current?.close();\n audioCtxRef.current = null;\n analyserRef.current = null;\n };\n }, [active]);\n\n const isLive = processing || active;\n\n return (\n \n {isLive && onStop && (\n \n \n \n )}\n
\n \n
\n \n );\n}\n", "type": "registry:ui", "target": "components/ruixen/live-waveform.tsx" } ] }