elevenlabs.json — ui Source File
Architecture documentation for elevenlabs.json, a json file in the ui codebase.
Entity Profile
Source Code
{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "elevenlabs",
"title": "Elevenlabs",
"registryDependencies": [
"example",
"button",
"card"
],
"files": [
{
"path": "registry/radix-mira/blocks/elevenlabs.tsx",
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { useEffect, useMemo, useRef, useState } from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport {\n Example,\n ExampleWrapper,\n} from \"@/registry/radix-mira/components/example\"\nimport { Button } from \"@/registry/radix-mira/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/registry/radix-mira/ui/card\"\n\nexport default function ElevenlabsBlocks() {\n return (\n <ExampleWrapper>\n <BarVisualizerDemo />\n <WaveformDemo />\n </ExampleWrapper>\n )\n}\n\nfunction BarVisualizerDemo() {\n const [state, setState] = useState<AgentState>(\"speaking\")\n\n return (\n <Example title=\"Bar Visualizer\">\n <Card className=\"\">\n <CardHeader>\n <CardTitle>Audio Frequency Visualizer</CardTitle>\n <CardDescription>\n Real-time frequency band visualization with animated state\n transitions\n </CardDescription>\n </CardHeader>\n <CardContent>\n <BarVisualizer\n state={state}\n demo={true}\n barCount={20}\n minHeight={15}\n maxHeight={90}\n className=\"h-40 max-w-full\"\n />\n </CardContent>\n <CardFooter className=\"gap-2\">\n <Button\n size=\"sm\"\n variant={state === \"connecting\" ? \"default\" : \"outline\"}\n onClick={() => setState(\"connecting\")}\n >\n Connecting\n </Button>\n <Button\n size=\"sm\"\n variant={state === \"listening\" ? \"default\" : \"outline\"}\n onClick={() => setState(\"listening\")}\n >\n Listening\n </Button>\n <Button\n size=\"sm\"\n variant={state === \"speaking\" ? \"default\" : \"outline\"}\n onClick={() => setState(\"speaking\")}\n >\n Speaking\n </Button>\n </CardFooter>\n </Card>\n </Example>\n )\n}\n\nfunction WaveformDemo() {\n const [active, setActive] = useState(false)\n const [processing, setProcessing] = useState(true)\n const [mode, setMode] = useState<\"static\" | \"scrolling\">(\"static\")\n const handleToggleActive = () => {\n setActive(!active)\n if (!active) {\n setProcessing(false)\n }\n }\n const handleToggleProcessing = () => {\n setProcessing(!processing)\n if (!processing) {\n setActive(false)\n }\n }\n return (\n <Example title=\"Waveform\" className=\"items-center justify-center\">\n <Card>\n <CardHeader>\n <CardTitle>Live Audio Waveform</CardTitle>\n <CardDescription>\n Real-time microphone input visualization with audio reactivity\n </CardDescription>\n </CardHeader>\n <CardContent>\n <LiveWaveform\n active={active}\n processing={processing}\n height={80}\n barWidth={3}\n barGap={2}\n mode={mode}\n fadeEdges={true}\n barColor=\"gray\"\n historySize={120}\n />\n </CardContent>\n <CardFooter className=\"gap-2\">\n <Button\n size=\"sm\"\n variant={active ? \"default\" : \"outline\"}\n onClick={handleToggleActive}\n >\n {active ? \"Stop\" : \"Start\"} Listening\n </Button>\n <Button\n size=\"sm\"\n variant={processing ? \"default\" : \"outline\"}\n onClick={handleToggleProcessing}\n >\n {processing ? \"Stop\" : \"Start\"} Processing\n </Button>\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => setMode(mode === \"static\" ? \"scrolling\" : \"static\")}\n >\n {mode === \"static\" ? \"Static\" : \"Scrolling\"}\n </Button>\n </CardFooter>\n </Card>\n </Example>\n )\n}\n\nexport interface AudioAnalyserOptions {\n fftSize?: number\n smoothingTimeConstant?: number\n minDecibels?: number\n maxDecibels?: number\n}\n\nfunction createAudioAnalyser(\n mediaStream: MediaStream,\n options: AudioAnalyserOptions = {}\n) {\n const audioContext = new (window.AudioContext ||\n (window as unknown as { webkitAudioContext: typeof AudioContext })\n .webkitAudioContext)()\n const source = audioContext.createMediaStreamSource(mediaStream)\n const analyser = audioContext.createAnalyser()\n\n if (options.fftSize) analyser.fftSize = options.fftSize\n if (options.smoothingTimeConstant !== undefined) {\n analyser.smoothingTimeConstant = options.smoothingTimeConstant\n }\n if (options.minDecibels !== undefined)\n analyser.minDecibels = options.minDecibels\n if (options.maxDecibels !== undefined)\n analyser.maxDecibels = options.maxDecibels\n\n source.connect(analyser)\n\n const cleanup = () => {\n source.disconnect()\n audioContext.close()\n }\n\n return { analyser, audioContext, cleanup }\n}\n\n/**\n * Hook for tracking the volume of an audio stream using the Web Audio API.\n * @param mediaStream - The MediaStream to analyze\n * @param options - Audio analyser options\n * @returns The current volume level (0-1)\n */\nexport function useAudioVolume(\n mediaStream?: MediaStream | null,\n options: AudioAnalyserOptions = { fftSize: 32, smoothingTimeConstant: 0 }\n) {\n const [volume, setVolume] = useState(0)\n const volumeRef = useRef(0)\n const frameId = useRef<number | undefined>(undefined)\n\n // Memoize options to prevent unnecessary re-renders\n const memoizedOptions = useMemo(() => options, [options])\n\n useEffect(() => {\n if (!mediaStream) {\n // eslint-disable-next-line react-hooks/set-state-in-effect\n setVolume(0)\n volumeRef.current = 0\n return\n }\n\n const { analyser, cleanup } = createAudioAnalyser(\n mediaStream,\n memoizedOptions\n )\n\n const bufferLength = analyser.frequencyBinCount\n const dataArray = new Uint8Array(bufferLength)\n let lastUpdate = 0\n const updateInterval = 1000 / 30 // 30 FPS\n\n const updateVolume = (timestamp: number) => {\n if (timestamp - lastUpdate >= updateInterval) {\n analyser.getByteFrequencyData(dataArray)\n let sum = 0\n for (let i = 0; i < dataArray.length; i++) {\n const a = dataArray[i]\n sum += a * a\n }\n const newVolume = Math.sqrt(sum / dataArray.length) / 255\n\n // Only update state if volume changed significantly\n if (Math.abs(newVolume - volumeRef.current) > 0.01) {\n volumeRef.current = newVolume\n setVolume(newVolume)\n }\n lastUpdate = timestamp\n }\n frameId.current = requestAnimationFrame(updateVolume)\n }\n\n frameId.current = requestAnimationFrame(updateVolume)\n\n return () => {\n cleanup()\n if (frameId.current) {\n cancelAnimationFrame(frameId.current)\n }\n }\n }, [mediaStream, memoizedOptions])\n\n return volume\n}\n\nexport interface MultiBandVolumeOptions {\n bands?: number\n loPass?: number // Low frequency cutoff\n hiPass?: number // High frequency cutoff\n updateInterval?: number // Update interval in ms\n analyserOptions?: AudioAnalyserOptions\n}\n\nconst multibandDefaults: MultiBandVolumeOptions = {\n bands: 5,\n loPass: 100,\n hiPass: 600,\n updateInterval: 32,\n analyserOptions: { fftSize: 2048 },\n}\n\n// Memoized normalization function to avoid recreating on each render\nconst normalizeDb = (value: number) => {\n if (value === -Infinity) return 0\n const minDb = -100\n const maxDb = -10\n const db = 1 - (Math.max(minDb, Math.min(maxDb, value)) * -1) / 100\n return Math.sqrt(db)\n}\n\n/**\n * Hook for tracking volume across multiple frequency bands\n * @param mediaStream - The MediaStream to analyze\n * @param options - Multiband options\n * @returns Array of volume levels for each frequency band\n */\nexport function useMultibandVolume(\n mediaStream?: MediaStream | null,\n options: MultiBandVolumeOptions = {}\n) {\n const opts = useMemo(() => ({ ...multibandDefaults, ...options }), [options])\n\n const [frequencyBands, setFrequencyBands] = useState<number[]>(() =>\n new Array(opts.bands).fill(0)\n )\n const bandsRef = useRef<number[]>(new Array(opts.bands).fill(0))\n const frameId = useRef<number | undefined>(undefined)\n\n useEffect(() => {\n if (!mediaStream) {\n const emptyBands = new Array(opts.bands).fill(0)\n setTimeout(() => {\n setFrequencyBands(emptyBands)\n }, 0)\n bandsRef.current = emptyBands\n return\n }\n\n const { analyser, cleanup } = createAudioAnalyser(\n mediaStream,\n opts.analyserOptions\n )\n\n const bufferLength = analyser.frequencyBinCount\n const dataArray = new Float32Array(bufferLength)\n const sliceStart = opts.loPass!\n const sliceEnd = opts.hiPass!\n const sliceLength = sliceEnd - sliceStart\n const chunkSize = Math.ceil(sliceLength / opts.bands!)\n\n let lastUpdate = 0\n const updateInterval = opts.updateInterval!\n\n const updateVolume = (timestamp: number) => {\n if (timestamp - lastUpdate >= updateInterval) {\n analyser.getFloatFrequencyData(dataArray)\n\n // Process directly without creating intermediate arrays\n const chunks = new Array(opts.bands!)\n\n for (let i = 0; i < opts.bands!; i++) {\n let sum = 0\n let count = 0\n const startIdx = sliceStart + i * chunkSize\n const endIdx = Math.min(sliceStart + (i + 1) * chunkSize, sliceEnd)\n\n for (let j = startIdx; j < endIdx; j++) {\n sum += normalizeDb(dataArray[j])\n count++\n }\n\n chunks[i] = count > 0 ? sum / count : 0\n }\n\n // Only update state if bands changed significantly\n let hasChanged = false\n for (let i = 0; i < chunks.length; i++) {\n if (Math.abs(chunks[i] - bandsRef.current[i]) > 0.01) {\n hasChanged = true\n break\n }\n }\n\n if (hasChanged) {\n bandsRef.current = chunks\n setFrequencyBands(chunks)\n }\n\n lastUpdate = timestamp\n }\n\n frameId.current = requestAnimationFrame(updateVolume)\n }\n\n frameId.current = requestAnimationFrame(updateVolume)\n\n return () => {\n cleanup()\n if (frameId.current) {\n cancelAnimationFrame(frameId.current)\n }\n }\n }, [mediaStream, opts])\n\n return frequencyBands\n}\n\ntype AnimationState =\n | \"connecting\"\n | \"initializing\"\n | \"listening\"\n | \"speaking\"\n | \"thinking\"\n | undefined\n\nexport const useBarAnimator = (\n state: AnimationState,\n columns: number,\n interval: number\n): number[] => {\n const indexRef = useRef(0)\n const [currentFrame, setCurrentFrame] = useState<number[]>([])\n const animationFrameId = useRef<number | null>(null)\n\n // Memoize sequence generation\n const sequence = useMemo(() => {\n if (state === \"thinking\" || state === \"listening\") {\n return generateListeningSequenceBar(columns)\n } else if (state === \"connecting\" || state === \"initializing\") {\n return generateConnectingSequenceBar(columns)\n } else if (state === undefined || state === \"speaking\") {\n return [new Array(columns).fill(0).map((_, idx) => idx)]\n } else {\n return [[]]\n }\n }, [state, columns])\n\n useEffect(() => {\n indexRef.current = 0\n setTimeout(() => {\n setCurrentFrame(sequence[0] || [])\n }, 0)\n }, [sequence])\n\n useEffect(() => {\n let startTime = performance.now()\n\n const animate = (time: DOMHighResTimeStamp) => {\n const timeElapsed = time - startTime\n\n if (timeElapsed >= interval) {\n indexRef.current = (indexRef.current + 1) % sequence.length\n setCurrentFrame(sequence[indexRef.current] || [])\n startTime = time\n }\n\n animationFrameId.current = requestAnimationFrame(animate)\n }\n\n animationFrameId.current = requestAnimationFrame(animate)\n\n return () => {\n if (animationFrameId.current !== null) {\n cancelAnimationFrame(animationFrameId.current)\n }\n }\n }, [interval, sequence])\n\n return currentFrame\n}\n\n// Memoize sequence generators\nconst generateConnectingSequenceBar = (columns: number): number[][] => {\n const seq = []\n for (let x = 0; x < columns; x++) {\n seq.push([x, columns - 1 - x])\n }\n return seq\n}\n\nconst generateListeningSequenceBar = (columns: number): number[][] => {\n const center = Math.floor(columns / 2)\n const noIndex = -1\n return [[center], [noIndex]]\n}\n\nexport type AgentState =\n | \"connecting\"\n | \"initializing\"\n | \"listening\"\n | \"speaking\"\n | \"thinking\"\n\nexport interface BarVisualizerProps\n extends React.HTMLAttributes<HTMLDivElement> {\n /** Voice assistant state */\n state?: AgentState\n /** Number of bars to display */\n barCount?: number\n /** Audio source */\n mediaStream?: MediaStream | null\n /** Min/max height as percentage */\n minHeight?: number\n maxHeight?: number\n /** Enable demo mode with fake audio data */\n demo?: boolean\n /** Align bars from center instead of bottom */\n centerAlign?: boolean\n}\n\nconst BarVisualizerComponent = React.forwardRef<\n HTMLDivElement,\n BarVisualizerProps\n>(\n (\n {\n state,\n barCount = 15,\n mediaStream,\n minHeight = 20,\n maxHeight = 100,\n demo = false,\n centerAlign = false,\n className,\n style,\n ...props\n },\n ref\n ) => {\n // Audio processing\n const realVolumeBands = useMultibandVolume(mediaStream, {\n bands: barCount,\n loPass: 100,\n hiPass: 200,\n })\n\n // Generate fake volume data for demo mode using refs to avoid state updates\n const fakeVolumeBandsRef = useRef<number[]>(new Array(barCount).fill(0.2))\n const [fakeVolumeBands, setFakeVolumeBands] = useState<number[]>(() =>\n new Array(barCount).fill(0.2)\n )\n const fakeAnimationRef = useRef<number | undefined>(undefined)\n\n // Animate fake volume bands for speaking and listening states\n useEffect(() => {\n if (!demo) return\n\n if (state !== \"speaking\" && state !== \"listening\") {\n const bands = new Array(barCount).fill(0.2)\n fakeVolumeBandsRef.current = bands\n setTimeout(() => {\n setFakeVolumeBands(bands)\n }, 0)\n return\n }\n\n let lastUpdate = 0\n const updateInterval = 50\n const startTime = Date.now() / 1000\n\n const updateFakeVolume = (timestamp: number) => {\n if (timestamp - lastUpdate >= updateInterval) {\n const time = Date.now() / 1000 - startTime\n const newBands = new Array(barCount)\n\n for (let i = 0; i < barCount; i++) {\n const waveOffset = i * 0.5\n const baseVolume = Math.sin(time * 2 + waveOffset) * 0.3 + 0.5\n const randomNoise = Math.random() * 0.2\n newBands[i] = Math.max(0.1, Math.min(1, baseVolume + randomNoise))\n }\n\n // Only update if values changed significantly\n let hasChanged = false\n for (let i = 0; i < barCount; i++) {\n if (Math.abs(newBands[i] - fakeVolumeBandsRef.current[i]) > 0.05) {\n hasChanged = true\n break\n }\n }\n\n if (hasChanged) {\n fakeVolumeBandsRef.current = newBands\n setFakeVolumeBands(newBands)\n }\n\n lastUpdate = timestamp\n }\n\n fakeAnimationRef.current = requestAnimationFrame(updateFakeVolume)\n }\n\n fakeAnimationRef.current = requestAnimationFrame(updateFakeVolume)\n\n return () => {\n if (fakeAnimationRef.current) {\n cancelAnimationFrame(fakeAnimationRef.current)\n }\n }\n }, [demo, state, barCount])\n\n // Use fake or real volume data based on demo mode\n const volumeBands = useMemo(\n () => (demo ? fakeVolumeBands : realVolumeBands),\n [demo, fakeVolumeBands, realVolumeBands]\n )\n\n // Animation sequencing\n const highlightedIndices = useBarAnimator(\n state,\n barCount,\n state === \"connecting\"\n ? 2000 / barCount\n : state === \"thinking\"\n ? 150\n : state === \"listening\"\n ? 500\n : 1000\n )\n\n return (\n <div\n ref={ref}\n data-state={state}\n className={cn(\n \"relative flex justify-center gap-1.5\",\n centerAlign ? \"items-center\" : \"items-end\",\n \"bg-muted h-32 w-full overflow-hidden rounded-lg p-4\",\n className\n )}\n style={{\n ...style,\n }}\n {...props}\n >\n {volumeBands.map((volume, index) => {\n const heightPct = Math.min(\n maxHeight,\n Math.max(minHeight, volume * 100 + 5)\n )\n const isHighlighted = highlightedIndices?.includes(index) ?? false\n\n return (\n <Bar\n key={index}\n heightPct={heightPct}\n isHighlighted={isHighlighted}\n state={state}\n />\n )\n })}\n </div>\n )\n }\n)\n\n// Memoized Bar component to prevent unnecessary re-renders\nconst Bar = React.memo<{\n heightPct: number\n isHighlighted: boolean\n state?: AgentState\n}>(({ heightPct, isHighlighted, state }) => (\n <div\n data-highlighted={isHighlighted}\n className={cn(\n \"max-w-[12px] min-w-[8px] flex-1 transition-all duration-150\",\n \"rounded-full\",\n \"bg-border data-[highlighted=true]:bg-primary\",\n state === \"speaking\" && \"bg-primary\",\n state === \"thinking\" && isHighlighted && \"animate-pulse\"\n )}\n style={{\n height: `${heightPct}%`,\n animationDuration: state === \"thinking\" ? \"300ms\" : undefined,\n }}\n />\n))\n\nBar.displayName = \"Bar\"\n\n// Wrap the main component with React.memo for prop comparison optimization\nconst BarVisualizer = React.memo(\n BarVisualizerComponent,\n (prevProps, nextProps) => {\n return (\n prevProps.state === nextProps.state &&\n prevProps.barCount === nextProps.barCount &&\n prevProps.mediaStream === nextProps.mediaStream &&\n prevProps.minHeight === nextProps.minHeight &&\n prevProps.maxHeight === nextProps.maxHeight &&\n prevProps.demo === nextProps.demo &&\n prevProps.centerAlign === nextProps.centerAlign &&\n prevProps.className === nextProps.className &&\n JSON.stringify(prevProps.style) === JSON.stringify(nextProps.style)\n )\n }\n)\n\nBarVisualizerComponent.displayName = \"BarVisualizerComponent\"\nBarVisualizer.displayName = \"BarVisualizer\"\n\nexport type LiveWaveformProps = React.HTMLAttributes<HTMLDivElement> & {\n active?: boolean\n processing?: boolean\n deviceId?: string\n barWidth?: number\n barHeight?: 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}\nexport const 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 barHeight: baseBarHeight = 4,\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 heightStyle = typeof height === \"number\" ? `${height}px` : height\n // Handle canvas resizing\n useEffect(() => {\n const canvas = canvasRef.current\n const container = containerRef.current\n if (!canvas || !container) return\n const resizeObserver = new ResizeObserver(() => {\n const rect = container.getBoundingClientRect()\n const dpr = window.devicePixelRatio || 1\n canvas.width = rect.width * dpr\n canvas.height = rect.height * dpr\n canvas.style.width = `${rect.width}px`\n canvas.style.height = `${rect.height}px`\n const ctx = canvas.getContext(\"2d\")\n if (ctx) {\n ctx.scale(dpr, dpr)\n }\n gradientCacheRef.current = null\n lastWidthRef.current = rect.width\n needsRedrawRef.current = true\n })\n resizeObserver.observe(container)\n return () => resizeObserver.disconnect()\n }, [])\n useEffect(() => {\n if (processing && !active) {\n let time = 0\n transitionProgressRef.current = 0\n const animateProcessing = () => {\n time += 0.03\n transitionProgressRef.current = Math.min(\n 1,\n transitionProgressRef.current + 0.02\n )\n const processingData = []\n const barCount = Math.floor(\n (containerRef.current?.getBoundingClientRect().width || 200) /\n (barWidth + barGap)\n )\n if (mode === \"static\") {\n const halfCount = Math.floor(barCount / 2)\n for (let i = 0; i < barCount; i++) {\n const normalizedPosition = (i - halfCount) / halfCount\n const centerWeight = 1 - Math.abs(normalizedPosition) * 0.4\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 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 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 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 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 processingData.push(Math.max(0.05, Math.min(1, finalValue)))\n }\n }\n if (mode === \"static\") {\n staticBarsRef.current = processingData\n } else {\n historyRef.current = processingData\n }\n needsRedrawRef.current = true\n processingAnimationRef.current =\n requestAnimationFrame(animateProcessing)\n }\n animateProcessing()\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 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 // 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 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 const AudioContextConstructor =\n window.AudioContext ||\n (window as unknown as { webkitAudioContext: typeof AudioContext })\n .webkitAudioContext\n const audioContext = new AudioContextConstructor()\n const analyser = audioContext.createAnalyser()\n analyser.fftSize = fftSize\n analyser.smoothingTimeConstant = smoothingTimeConstant\n const source = audioContext.createMediaStreamSource(stream)\n source.connect(analyser)\n audioContextRef.current = audioContext\n analyserRef.current = analyser\n // Clear history when starting\n historyRef.current = []\n } catch (error) {\n onError?.(error as Error)\n }\n }\n setupMicrophone()\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 // Animation loop\n useEffect(() => {\n const canvas = canvasRef.current\n if (!canvas) return\n const ctx = canvas.getContext(\"2d\")\n if (!ctx) return\n let rafId: number\n const animate = (currentTime: number) => {\n // Render waveform\n const rect = canvas.getBoundingClientRect()\n // Update audio data if active\n if (active && currentTime - lastUpdateRef.current > updateRate) {\n lastUpdateRef.current = currentTime\n if (analyserRef.current) {\n const dataArray = new Uint8Array(\n analyserRef.current.frequencyBinCount\n )\n analyserRef.current.getByteFrequencyData(dataArray)\n if (mode === \"static\") {\n // For static mode, update bars in place\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 const barCount = Math.floor(rect.width / (barWidth + barGap))\n const halfCount = Math.floor(barCount / 2)\n const newBars: number[] = []\n // Mirror the data for symmetric display\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 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 staticBarsRef.current = newBars\n lastActiveDataRef.current = newBars\n } else {\n // Scrolling mode - original behavior\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 for (let i = 0; i < relevantData.length; i++) {\n sum += relevantData[i]\n }\n const average = (sum / relevantData.length / 255) * sensitivity\n // Add to history\n historyRef.current.push(Math.min(1, Math.max(0.05, average)))\n lastActiveDataRef.current = [...historyRef.current]\n // Maintain history size\n if (historyRef.current.length > historySize) {\n historyRef.current.shift()\n }\n }\n needsRedrawRef.current = true\n }\n }\n // Only redraw if needed\n if (!needsRedrawRef.current && !active) {\n rafId = requestAnimationFrame(animate)\n return\n }\n needsRedrawRef.current = active\n ctx.clearRect(0, 0, rect.width, rect.height)\n const computedBarColor =\n barColor ||\n (() => {\n const style = getComputedStyle(canvas)\n // Try to get the computed color value directly\n const color = style.color\n return color || \"#000\"\n })()\n const step = barWidth + barGap\n const barCount = Math.floor(rect.width / step)\n const centerY = rect.height / 2\n // Draw bars based on mode\n if (mode === \"static\") {\n // Static mode - bars in fixed positions\n const dataToRender = processing\n ? staticBarsRef.current\n : active\n ? staticBarsRef.current\n : staticBarsRef.current.length > 0\n ? staticBarsRef.current\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(baseBarHeight, value * rect.height * 0.8)\n const y = centerY - barHeight / 2\n ctx.fillStyle = computedBarColor\n ctx.globalAlpha = 0.4 + value * 0.6\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 // Scrolling mode - original behavior\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 = rect.width - (i + 1) * step\n const barHeight = Math.max(baseBarHeight, value * rect.height * 0.8)\n const y = centerY - barHeight / 2\n ctx.fillStyle = computedBarColor\n ctx.globalAlpha = 0.4 + value * 0.6\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 // Apply edge fading\n if (fadeEdges && fadeWidth > 0 && rect.width > 0) {\n // Cache gradient if width hasn't changed\n if (!gradientCacheRef.current || lastWidthRef.current !== rect.width) {\n const gradient = ctx.createLinearGradient(0, 0, rect.width, 0)\n const fadePercent = Math.min(0.3, fadeWidth / rect.width)\n // destination-out: removes destination where source alpha is high\n // We want: fade edges out, keep center solid\n // Left edge: start opaque (1) = remove, fade to transparent (0) = keep\n gradient.addColorStop(0, \"rgba(255,255,255,1)\")\n gradient.addColorStop(fadePercent, \"rgba(255,255,255,0)\")\n // Center stays transparent = keep everything\n gradient.addColorStop(1 - fadePercent, \"rgba(255,255,255,0)\")\n // Right edge: fade from transparent (0) = keep to opaque (1) = remove\n gradient.addColorStop(1, \"rgba(255,255,255,1)\")\n gradientCacheRef.current = gradient\n lastWidthRef.current = rect.width\n }\n ctx.globalCompositeOperation = \"destination-out\"\n ctx.fillStyle = gradientCacheRef.current\n ctx.fillRect(0, 0, rect.width, rect.height)\n ctx.globalCompositeOperation = \"source-over\"\n }\n ctx.globalAlpha = 1\n rafId = requestAnimationFrame(animate)\n }\n rafId = requestAnimationFrame(animate)\n return () => {\n if (rafId) {\n cancelAnimationFrame(rafId)\n }\n }\n }, [\n active,\n processing,\n sensitivity,\n updateRate,\n historySize,\n barWidth,\n baseBarHeight,\n barGap,\n barRadius,\n barColor,\n fadeEdges,\n fadeWidth,\n mode,\n ])\n return (\n <div\n className={cn(\"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",
"type": "registry:block"
}
],
"type": "registry:block"
}
Source
Frequently Asked Questions
What does elevenlabs.json do?
elevenlabs.json is a source file in the ui codebase, written in json.
Where is elevenlabs.json in the architecture?
elevenlabs.json is located at apps/v4/public/r/styles/radix-mira/elevenlabs.json (directory: apps/v4/public/r/styles/radix-mira).
Analyze Your Own Codebase
Get architecture documentation, dependency graphs, and domain analysis for your codebase in minutes.
Try Supermodel Free