{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"agent-audio-visualizer-wave","title":"Agent Audio Visualizer Wave","description":"A wave visualizer for audio tracks.","dependencies":["livekit-client@^2.0.0","@livekit/components-react@^2.0.0","class-variance-authority"],"registryDependencies":["utils","@agents-ui/react-shader-toy"],"files":[{"path":"components/agents-ui/agent-audio-visualizer-wave.tsx","content":"'use client';\n\nimport { useMemo, type ComponentProps } from 'react';\nimport { type VariantProps, cva } from 'class-variance-authority';\nimport { type AgentState, type TrackReferenceOrPlaceholder } from '@livekit/components-react';\n\nimport { ReactShaderToy } from '@/components/agents-ui/react-shader-toy';\nimport { useAgentAudioVisualizerWave } from '@/hooks/agents-ui/use-agent-audio-visualizer-wave';\nimport { cn } from '@/lib/utils';\nimport type { LocalAudioTrack, RemoteAudioTrack } from 'livekit-client';\n\nconst DEFAULT_COLOR = '#1FD5F9';\n\nfunction hexToRgb(hexColor: string) {\n  try {\n    const rgbColor = hexColor.match(/^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/);\n\n    if (rgbColor) {\n      const [, r, g, b] = rgbColor;\n      const color = [r, g, b].map((c = '00') => parseInt(c, 16) / 255);\n\n      return color;\n    }\n  } catch {\n    console.error(\n      `Invalid hex color '${hexColor}'.\\nFalling back to default color '${DEFAULT_COLOR}'.`,\n    );\n  }\n\n  return hexToRgb(DEFAULT_COLOR);\n}\n\nconst shaderSource = `\nconst float TAU = 6.28318530718;\n\n// Noise for dithering\nvec2 randFibo(vec2 p) {\n  p = fract(p * vec2(443.897, 441.423));\n  p += dot(p, p.yx + 19.19);\n  return fract((p.xx + p.yx) * p.xy);\n}\n\n// Luma for alpha\nfloat luma(vec3 color) {\n  return dot(color, vec3(0.299, 0.587, 0.114));\n}\n\n// RGB to HSV\nvec3 rgb2hsv(vec3 c) {\n  vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);\n  vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));\n  vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));\n  float d = q.x - min(q.w, q.y);\n  float e = 1.0e-10;\n  return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);\n}\n\n// HSV to RGB\nvec3 hsv2rgb(vec3 c) {\n  vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);\n  vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);\n  return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);\n}\n\n// Bell curve function for attenuation from center with rounded top\nfloat bellCurve(float distanceFromCenter, float maxDistance) {\n  float normalizedDistance = distanceFromCenter / maxDistance;\n  // Use cosine with high power for smooth rounded top\n  return pow(cos(normalizedDistance * (3.14159265359 / 4.0)), 16.0);\n}\n\n// Calculate the sine wave\nfloat oscilloscopeWave(float x, float centerX, float time) {\n  float relativeX = x - centerX;\n  float maxDistance = centerX;\n  float distanceFromCenter = abs(relativeX);\n\n  // Apply bell curve for amplitude attenuation\n  float bell = bellCurve(distanceFromCenter, maxDistance);\n\n  // Calculate wave with uniforms and bell curve attenuation\n  float wave = sin(relativeX * uFrequency + time * uSpeed) * uAmplitude * bell;\n\n  return wave;\n}\n\nvoid mainImage(out vec4 fragColor, in vec2 fragCoord) {\n  vec2 uv = fragCoord / iResolution.xy;\n  vec2 pos = uv - 0.5;\n\n  // Calculate center and positions\n  float centerX = 0.5;\n  float centerY = 0.5;\n  float x = uv.x;\n  float y = uv.y;\n\n  // Convert line width from pixels to UV space\n  // Use the average of width and height to handle aspect ratio\n  float pixelSize = 2.0 / (iResolution.x + iResolution.y);\n  float lineWidthUV = uLineWidth * pixelSize;\n  float smoothingUV = uSmoothing * pixelSize;\n\n  // Find minimum distance to the wave by sampling nearby points\n  // This gives us consistent line width without high-frequency artifacts\n  const int NUM_SAMPLES = 50; // Must be const for GLSL loop\n  float minDist = 1000.0;\n  float sampleRange = 0.02; // Range to search for closest point\n\n  for(int i = 0; i < NUM_SAMPLES; i++) {\n    float offset = (float(i) / float(NUM_SAMPLES - 1) - 0.5) * sampleRange;\n    float sampleX = x + offset;\n    float waveY = centerY + oscilloscopeWave(sampleX, centerX, iTime);\n\n    // Calculate distance from current pixel to this point on the wave\n    vec2 wavePoint = vec2(sampleX, waveY);\n    vec2 currentPoint = vec2(x, y);\n    float dist = distance(currentPoint, wavePoint);\n\n    minDist = min(minDist, dist);\n  }\n\n  // Solid line with smooth edges using minimum distance\n  float line = smoothstep(lineWidthUV + smoothingUV, lineWidthUV - smoothingUV, minDist);\n\n  vec3 color = uColor;\n  if(abs(uColorShift) > 0.01) {\n    // Keep the center 50% at base color, then ramp shift across outer 25% on each side.\n    float centerBandHalfWidth = 0.2;\n    float edgeBandWidth = 0.5;\n    float distanceFromCenter = abs(x - centerX);\n    float edgeFactor = clamp((distanceFromCenter - centerBandHalfWidth) / edgeBandWidth, 0.0, 1.0);\n    vec3 hsv = rgb2hsv(color);\n    // Hue shift is zero in the center band and strongest at far edges.\n    hsv.x = fract(hsv.x + edgeFactor * uColorShift * 0.3);\n    color = hsv2rgb(hsv);\n  }\n\n  // Apply line intensity\n  color *= line;\n\n  // Add dithering for smoother gradients\n  // color += (randFibo(fragCoord).x - 0.5) / 255.0;\n\n  // Calculate alpha based on line intensity\n  float alpha = line * uMix;\n\n  fragColor = vec4(color * uMix, alpha);\n}`;\n\ninterface WaveShaderProps {\n  /**\n   * Class name\n   * @default ''\n   */\n  className?: string;\n  /**\n   * Speed of the oscilloscope\n   * @default 10\n   */\n  speed?: number;\n  /**\n   * Amplitude of the oscilloscope\n   * @default 0.02\n   */\n  amplitude?: number;\n  /**\n   * Frequency of the oscilloscope\n   * @default 20.0\n   */\n  frequency?: number;\n  /**\n   * Color of the oscilloscope in hexidecimal format.\n   * @default '#1FD5F9'\n   */\n  color?: `#${string}`;\n  /**\n   * Hue shift amount applied toward the outside of the wave. Center remains at the base color.\n   * @default 0.05\n   */\n  colorShift?: number;\n  /**\n   * Mix of the oscilloscope\n   * @default 1.0\n   */\n  mix?: number;\n  /**\n   * Line width of the oscilloscope in pixels\n   * @default 2.0\n   */\n  lineWidth?: number;\n  /**\n   * Blur of the oscilloscope in pixels\n   * @default 0.5\n   */\n  blur?: number;\n}\n\nfunction WaveShader({\n  speed = 10,\n  color = '#1FD5F9',\n  colorShift = 0.05,\n  mix = 1.0,\n  amplitude = 0.02,\n  frequency = 20.0,\n  lineWidth = 2.0,\n  blur = 0.5,\n  ref,\n  className,\n  ...props\n}: WaveShaderProps & ComponentProps<'div'>) {\n  const rgbColor = useMemo(() => hexToRgb(color), [color]);\n\n  return (\n    <div ref={ref} className={className} {...props}>\n      <ReactShaderToy\n        fs={shaderSource}\n        devicePixelRatio={globalThis.devicePixelRatio ?? 1}\n        uniforms={{\n          uSpeed: { type: '1f', value: speed },\n          uAmplitude: { type: '1f', value: amplitude },\n          uFrequency: { type: '1f', value: frequency },\n          uMix: { type: '1f', value: mix },\n          uLineWidth: { type: '1f', value: lineWidth },\n          uSmoothing: { type: '1f', value: blur },\n          uColor: { type: '3fv', value: rgbColor },\n          uColorShift: { type: '1f', value: colorShift },\n        }}\n        onError={(error) => {\n          console.error('Shader error:', error);\n        }}\n        onWarning={(warning) => {\n          console.warn('Shader warning:', warning);\n        }}\n        style={{ width: '100%', height: '100%' }}\n      />\n    </div>\n  );\n}\n\nWaveShader.displayName = 'WaveShader';\n\nexport const AgentAudioVisualizerWaveVariants = cva(['aspect-square'], {\n  variants: {\n    size: {\n      icon: 'h-[24px]',\n      sm: 'h-[56px]',\n      md: 'h-[112px]',\n      lg: 'h-[224px]',\n      xl: 'h-[448px]',\n    },\n  },\n  defaultVariants: {\n    size: 'lg',\n  },\n});\n\nexport interface AgentAudioVisualizerWaveProps {\n  /**\n   * The size of the visualizer.\n   * @defaultValue 'lg'\n   */\n  size?: 'icon' | 'sm' | 'md' | 'lg' | 'xl';\n  /**\n   * The agent state.\n   * @defaultValue 'speaking'\n   */\n  state?: AgentState;\n  /**\n   * The color of the wave in hexidecimal format.\n   * @defaultValue '#1FD5F9'\n   */\n  color?: `#${string}`;\n  /**\n   * The color shift of the wave. Higher values increase hue variation toward the edges.\n   * @defaultValue 0.05\n   */\n  colorShift?: number;\n  /**\n   * The line width of the wave in pixels.\n   * @defaultValue 2.0\n   */\n  lineWidth?: number;\n  /**\n   * The blur of the wave in pixels.\n   * @defaultValue 0.5\n   */\n  blur?: number;\n  /**\n   * The audio track to visualize. Can be a local/remote audio track or a track reference.\n   */\n  audioTrack?: LocalAudioTrack | RemoteAudioTrack | TrackReferenceOrPlaceholder;\n  /**\n   * Volume value (0-1) to use instead of the value computed from the audioTrack.\n   */\n  volume?: number;\n  /**\n   * Additional CSS class names to apply to the container.\n   */\n  className?: string;\n}\n\n/**\n * A wave-style audio visualizer that responds to agent state and audio levels.\n * Displays an animated wave that reacts to the current agent state (connecting, thinking, speaking, etc.)\n * and audio volume when speaking.\n *\n * @extends ComponentProps<'div'>\n *\n * @example ```tsx\n * <AgentAudioVisualizerWave\n *   size=\"lg\"\n *   state=\"speaking\"\n *   color=\"#1FD5F9\"\n *   colorShift={0.3}\n *   lineWidth={2}\n *   blur={0.5}\n *   audioTrack={audioTrack}\n * />\n * ```\n */\nexport function AgentAudioVisualizerWave({\n  size = 'lg',\n  state = 'speaking',\n  color,\n  colorShift = 0.05,\n  lineWidth,\n  blur,\n  audioTrack,\n  volume,\n  className,\n  ref,\n  ...props\n}: AgentAudioVisualizerWaveProps &\n  ComponentProps<'div'> &\n  VariantProps<typeof AgentAudioVisualizerWaveVariants>) {\n  const _lineWidth = useMemo(() => {\n    if (lineWidth !== undefined) {\n      return lineWidth;\n    }\n    switch (size) {\n      case 'icon':\n      case 'sm':\n        return 2;\n      default:\n        return 1;\n    }\n  }, [lineWidth, size]);\n\n  const { speed, amplitude, frequency, opacity } = useAgentAudioVisualizerWave({\n    state,\n    audioTrack,\n    volume,\n  });\n\n  return (\n    <WaveShader\n      ref={ref}\n      data-lk-state={state}\n      speed={speed}\n      color={color}\n      colorShift={colorShift}\n      mix={opacity}\n      amplitude={amplitude}\n      frequency={frequency}\n      lineWidth={_lineWidth}\n      blur={blur}\n      className={cn(\n        AgentAudioVisualizerWaveVariants({ size }),\n        'mask-[linear-gradient(90deg,transparent_0%,black_20%,black_80%,transparent_100%)]',\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n","type":"registry:component"},{"path":"hooks/agents-ui/use-agent-audio-visualizer-wave.ts","content":"import { useRef, useState, useEffect, useCallback } from 'react';\nimport {\n  type AnimationPlaybackControlsWithThen,\n  type ValueAnimationTransition,\n  animate,\n  useMotionValue,\n  useMotionValueEvent,\n} from 'motion/react';\nimport {\n  type AgentState,\n  type TrackReference,\n  type TrackReferenceOrPlaceholder,\n  useTrackVolume,\n} from '@livekit/components-react';\nimport { LocalAudioTrack, RemoteAudioTrack } from 'livekit-client';\n\nconst DEFAULT_SPEED = 5;\nconst DEFAULT_AMPLITUDE = 0.025;\nconst DEFAULT_FREQUENCY = 10;\nconst DEFAULT_TRANSITION: ValueAnimationTransition = { duration: 0.2, ease: 'easeOut' };\n\nfunction useAnimatedValue<T>(initialValue: T) {\n  const [value, setValue] = useState(initialValue);\n  const motionValue = useMotionValue(initialValue);\n  const controlsRef = useRef<AnimationPlaybackControlsWithThen | null>(null);\n  useMotionValueEvent(motionValue, 'change', (value) => setValue(value as T));\n\n  const animateFn = useCallback(\n    (targetValue: T | T[], transition: ValueAnimationTransition) => {\n      controlsRef.current = animate(motionValue, targetValue, transition);\n    },\n    [motionValue],\n  );\n\n  return { value, controls: controlsRef, animate: animateFn };\n}\n\ninterface UseAgentAudioVisualizerWaveAnimatorArgs {\n  state?: AgentState;\n  audioTrack?: LocalAudioTrack | RemoteAudioTrack | TrackReferenceOrPlaceholder;\n  volume?: number;\n}\n\nexport function useAgentAudioVisualizerWave({\n  state,\n  audioTrack,\n  volume: volumeProp,\n}: UseAgentAudioVisualizerWaveAnimatorArgs) {\n  const [speed, setSpeed] = useState(DEFAULT_SPEED);\n  const { value: amplitude, animate: animateAmplitude } = useAnimatedValue(DEFAULT_AMPLITUDE);\n  const { value: frequency, animate: animateFrequency } = useAnimatedValue(DEFAULT_FREQUENCY);\n  const { value: opacity, animate: animateOpacity } = useAnimatedValue(1.0);\n\n  const trackVolume = useTrackVolume(audioTrack as TrackReference, {\n    fftSize: 512,\n    smoothingTimeConstant: 0.55,\n  });\n  const volume = volumeProp ?? trackVolume;\n\n  useEffect(() => {\n    switch (state) {\n      case 'disconnected':\n        setSpeed(DEFAULT_SPEED);\n        animateAmplitude(0, DEFAULT_TRANSITION);\n        animateFrequency(0, DEFAULT_TRANSITION);\n        animateOpacity(1.0, DEFAULT_TRANSITION);\n        return;\n      case 'listening':\n        setSpeed(DEFAULT_SPEED);\n        animateAmplitude(DEFAULT_AMPLITUDE, DEFAULT_TRANSITION);\n        animateFrequency(DEFAULT_FREQUENCY, DEFAULT_TRANSITION);\n        animateOpacity([1.0, 0.3], {\n          duration: 0.75,\n          repeat: Infinity,\n          repeatType: 'mirror',\n        });\n        return;\n      case 'thinking':\n      case 'connecting':\n      case 'initializing':\n        setSpeed(DEFAULT_SPEED * 4);\n        animateAmplitude(DEFAULT_AMPLITUDE / 4, DEFAULT_TRANSITION);\n        animateFrequency(DEFAULT_FREQUENCY * 4, DEFAULT_TRANSITION);\n        animateOpacity([1.0, 0.3], {\n          duration: 0.4,\n          repeat: Infinity,\n          repeatType: 'mirror',\n        });\n        return;\n      case 'speaking':\n      default:\n        setSpeed(DEFAULT_SPEED * 2);\n        animateAmplitude(DEFAULT_AMPLITUDE, DEFAULT_TRANSITION);\n        animateFrequency(DEFAULT_FREQUENCY, DEFAULT_TRANSITION);\n        animateOpacity(1.0, DEFAULT_TRANSITION);\n        return;\n    }\n  }, [state, setSpeed, animateAmplitude, animateFrequency, animateOpacity]);\n\n  useEffect(() => {\n    if (state === 'speaking') {\n      animateAmplitude(0.015 + 0.4 * volume, { duration: 0 });\n      animateFrequency(20 + 60 * volume, { duration: 0 });\n    }\n  }, [state, volume, animateAmplitude, animateFrequency]);\n\n  return {\n    speed,\n    amplitude,\n    frequency,\n    opacity,\n  };\n}\n","type":"registry:hook"}],"type":"registry:component"}