{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"agent-audio-visualizer-aura","title":"Agent Audio Visualizer Aura","description":"An aura 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-aura.tsx","content":"'use client';\n\n/**\n * @license\n *\n * Originally developed for Unicorn Studio\n * https://unicorn.studio\n *\n * Licensed under the Polyform Non-Resale License 1.0.0\n * https://polyformproject.org/licenses/non-resale/1.0.0/\n *\n * © 2026 UNCRN LLC\n */\n\nimport React, { useMemo, type ComponentProps } from 'react';\nimport { type VariantProps, cva } from 'class-variance-authority';\nimport { type LocalAudioTrack, type RemoteAudioTrack } from 'livekit-client';\nimport { type AgentState, type TrackReferenceOrPlaceholder } from '@livekit/components-react';\n\nimport { ReactShaderToy } from '@/components/agents-ui/react-shader-toy';\nimport { useAgentAudioVisualizerAura } from '@/hooks/agents-ui/use-agent-audio-visualizer-aura';\nimport { cn } from '@/lib/utils';\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 (error) {\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.283185;\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// Tonemap\nvec3 Tonemap(vec3 x) {\n  x *= 4.0;\n  return x / (1.0 + x);\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// SDF shapes\nfloat sdCircle(vec2 st, float r) {\n  return length(st) - r;\n}\n\nfloat sdLine(vec2 p, float r) {\n  float halfLen = r * 2.0;\n  vec2 a = vec2(-halfLen, 0.0);\n  vec2 b = vec2(halfLen, 0.0);\n  vec2 pa = p - a;\n  vec2 ba = b - a;\n  float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);\n  return length(pa - ba * h);\n}\n\nfloat getSdf(vec2 st) {\n  if(uShape == 1.0) return sdCircle(st, uScale);\n  else if(uShape == 2.0) return sdLine(st, uScale);\n  return sdCircle(st, uScale); // Default\n}\n\nvec2 turb(vec2 pos, float t, float it) {\n  // Initial rotation matrix for swirl direction\n  mat2 rotation = mat2(0.6, -0.25, 0.25, 0.9);\n  // Secondary rotation applied each iteration (approx 53 degree rotation)\n  mat2 layerRotation = mat2(0.6, -0.8, 0.8, 0.6);\n\n  float frequency = mix(2.0, 15.0, uFrequency);\n  float amplitude = uAmplitude;\n  float frequencyGrowth = 1.4;\n  float animTime = t * 0.1 * uSpeed;\n\n  const int LAYERS = 4;\n  for(int i = 0; i < LAYERS; i++) {\n    // Calculate wave displacement for this layer\n    vec2 rotatedPos = pos * rotation;\n    vec2 wave = sin(frequency * rotatedPos + float(i) * animTime + it);\n\n    // Apply displacement along rotation direction\n    pos += (amplitude / frequency) * rotation[0] * wave;\n\n    // Evolve parameters for next layer\n    rotation *= layerRotation;\n    amplitude *= mix(1.0, max(wave.x, wave.y), uVariance);\n    frequency *= frequencyGrowth;\n  }\n\n  return pos;\n}\n\nconst float ITERATIONS = 36.0;\n\nvoid mainImage(out vec4 fragColor, in vec2 fragCoord) {\n  vec2 uv = fragCoord / iResolution.xy;\n\n  vec3 pp = vec3(0.0);\n  vec3 bloom = vec3(0.0);\n  float t = iTime * 0.5;\n  vec2 pos = uv - 0.5;\n\n  vec2 prevPos = turb(pos, t, 0.0 - 1.0 / ITERATIONS);\n  float spacing = mix(1.0, TAU, uSpacing);\n\n  for(float i = 1.0; i < ITERATIONS + 1.0; i++) {\n    float iter = i / ITERATIONS;\n    vec2 st = turb(pos, t, iter * spacing);\n    float d = abs(getSdf(st));\n    float pd = distance(st, prevPos);\n    prevPos = st;\n    float dynamicBlur = exp2(pd * 2.0 * 1.4426950408889634) - 1.0;\n    float ds = smoothstep(0.0, uBlur * 0.05 + max(dynamicBlur * uSmoothing, 0.001), d);\n\n    // Shift color based on iteration using uColorScale\n    vec3 color = uColor;\n    if(uColorShift > 0.01) {\n      vec3 hsv = rgb2hsv(color);\n      // Shift hue by iteration\n      hsv.x = fract(hsv.x + (1.0 - iter) * uColorShift * 0.3);\n      color = hsv2rgb(hsv);\n    }\n\n    float invd = 1.0 / max(d + dynamicBlur, 0.001);\n    pp += (ds - 1.0) * color;\n    bloom += clamp(invd, 0.0, 250.0) * color;\n  }\n\n  pp *= 1.0 / ITERATIONS;\n\n  vec3 color;\n\n  // Dark mode (default)\n  if(uMode < 0.5) {\n    // use bloom effect\n    bloom = bloom / (bloom + 2e4);\n    color = (-pp + bloom * 3.0 * uBloom) * 1.2;\n    color += (randFibo(fragCoord).x - 0.5) / 255.0;\n    color = Tonemap(color);\n    float alpha = luma(color) * uMix;\n    fragColor = vec4(color * uMix, alpha);\n  }\n\n  // Light mode\n  else {\n    // no bloom effect\n    color = -pp;\n    color += (randFibo(fragCoord).x - 0.5) / 255.0;\n\n    // Preserve hue by tone mapping brightness only\n    float brightness = length(color);\n    vec3 direction = brightness > 0.0 ? color / brightness : color;\n\n    // Reinhard on brightness\n    float factor = 2.0;\n    float mappedBrightness = (brightness * factor) / (1.0 + brightness * factor);\n    color = direction * mappedBrightness;\n\n    // Boost saturation to compensate for white background bleed-through\n    // When alpha < 1.0, white bleeds through making colors look desaturated\n    // So we increase saturation to maintain vibrant appearance\n    float gray = dot(color, vec3(0.2, 0.5, 0.1));\n    float saturationBoost = 3.0;\n    color = mix(vec3(gray), color, saturationBoost);\n\n    // Clamp between 0-1\n    color = clamp(color, 0.0, 1.0);\n\n    float alpha = mappedBrightness * clamp(uMix, 1.0, 2.0);\n    fragColor = vec4(color, alpha);\n  }\n}`;\n\ninterface AuraShaderProps {\n  /**\n   * Aurora wave speed\n   * @default 1.0\n   */\n  speed?: number;\n\n  /**\n   * Turbulence amplitude\n   * @default 0.5\n   */\n  amplitude?: number;\n\n  /**\n   * Wave frequency and complexity\n   * @default 0.5\n   */\n  frequency?: number;\n\n  /**\n   * Shape scale\n   * @default 0.3\n   */\n  scale?: number;\n\n  /**\n   * Shape type: 1=circle, 2=line\n   * @default 1\n   */\n  shape?: number;\n\n  /**\n   * Edge blur/softness\n   * @default 1.0\n   */\n  blur?: number;\n\n  /**\n   * Color of the aura in hexidecimal format.\n   * @default '#1FD5F9'\n   */\n  color?: `#${string}`;\n\n  /**\n   * Color variation across layers (0-1)\n   * Controls how much colors change between iterations\n   * @default 0.5\n   * @example 0.0 - minimal color variation (more uniform)\n   * @example 0.5 - moderate variation (default)\n   * @example 1.0 - maximum variation (rainbow effect)\n   */\n  colorShift?: number;\n\n  /**\n   * Brightness of the aurora (0-1)\n   * @default 1.0\n   */\n  brightness?: number;\n\n  /**\n   * Display mode for different backgrounds\n   * - 'dark': Optimized for dark backgrounds (default)\n   * - 'light': Optimized for light/white backgrounds (inverts colors)\n   * @default 'dark'\n   */\n  themeMode?: 'dark' | 'light';\n}\n\nfunction AuraShader({\n  shape = 1.0,\n  speed = 1.0,\n  amplitude = 0.5,\n  frequency = 0.5,\n  scale = 0.2,\n  blur = 1.0,\n  color = DEFAULT_COLOR,\n  colorShift = 1.0,\n  brightness = 1.0,\n  themeMode = typeof window !== 'undefined' && document.documentElement.classList.contains('dark')\n    ? 'dark'\n    : 'light',\n  ref,\n  className,\n  ...props\n}: AuraShaderProps & 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          // Aurora wave speed\n          uSpeed: { type: '1f', value: speed },\n          // Edge blur/softness\n          uBlur: { type: '1f', value: blur },\n          // Shape scale\n          uScale: { type: '1f', value: scale },\n          // Shape type: 1=circle, 2=line\n          uShape: { type: '1f', value: shape },\n          // Wave frequency and complexity\n          uFrequency: { type: '1f', value: frequency },\n          // Turbulence amplitude\n          uAmplitude: { type: '1f', value: amplitude },\n          // Light intensity (bloom)\n          uBloom: { type: '1f', value: 0.0 },\n          // Brightness of the aurora (0-1)\n          uMix: { type: '1f', value: brightness },\n          // Color variation across layers (0-1)\n          uSpacing: { type: '1f', value: 0.5 },\n          // Color palette offset - shifts colors along the gradient (0-1)\n          uColorShift: { type: '1f', value: colorShift },\n          // Color variation across layers (0-1)\n          uVariance: { type: '1f', value: 0.1 },\n          // Smoothing of the aurora (0-1)\n          uSmoothing: { type: '1f', value: 1.0 },\n          // Display mode: 0=dark background, 1=light background\n          uMode: { type: '1f', value: themeMode === 'light' ? 1.0 : 0.0 },\n          // Color\n          uColor: { type: '3fv', value: rgbColor ?? [0, 0.7, 1] },\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\nAuraShader.displayName = 'AuraShader';\n\nexport const AgentAudioVisualizerAuraVariants = cva(['aspect-square'], {\n  variants: {\n    size: {\n      icon: 'h-[24px] gap-[2px]',\n      sm: 'h-[56px] gap-[4px]',\n      md: 'h-[112px] gap-[8px]',\n      lg: 'h-[224px] gap-[16px]',\n      xl: 'h-[448px] gap-[32px]',\n    },\n  },\n  defaultVariants: {\n    size: 'md',\n  },\n});\n\nexport interface AgentAudioVisualizerAuraProps {\n  /**\n   * The size of the visualizer.\n   * @defaultValue 'lg'\n   */\n  size?: 'icon' | 'sm' | 'md' | 'lg' | 'xl';\n  /**\n   * Agent state\n   * @default 'connecting'\n   */\n  state?: AgentState;\n  /**\n   * The color of the aura in hexidecimal format.\n   * @defaultValue '#1FD5F9'\n   */\n  color?: `#${string}`;\n  /**\n   * The color shift of the aura.\n   * @defaultValue 0.05\n   */\n  colorShift?: number;\n  /**\n   * The theme mode of the aura.\n   * @defaultValue 'dark'\n   */\n  themeMode?: 'dark' | 'light';\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\n/**\n * An shader-based audio visualizer that responds to agent state and audio levels.\n * Displays an animated elliptical aura 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\n * ```tsx\n * <AgentAudioVisualizerAura\n *   size=\"md\"\n *   state=\"speaking\"\n *   audioTrack={agentAudioTrack}\n * />\n * ```\n */\nexport function AgentAudioVisualizerAura({\n  size = 'lg',\n  state = 'connecting',\n  color = DEFAULT_COLOR,\n  colorShift = 0.05,\n  audioTrack,\n  volume,\n  themeMode,\n  className,\n  ref,\n  ...props\n}: AgentAudioVisualizerAuraProps &\n  ComponentProps<'div'> &\n  VariantProps<typeof AgentAudioVisualizerAuraVariants>) {\n  const { speed, scale, amplitude, frequency, brightness } = useAgentAudioVisualizerAura(\n    state,\n    audioTrack,\n    volume,\n  );\n\n  return (\n    <AuraShader\n      ref={ref}\n      data-lk-state={state}\n      blur={0.2}\n      color={color}\n      colorShift={colorShift}\n      speed={speed}\n      scale={scale}\n      themeMode={themeMode}\n      amplitude={amplitude}\n      frequency={frequency}\n      brightness={brightness}\n      className={cn(AgentAudioVisualizerAuraVariants({ size }), className)}\n      {...props}\n    />\n  );\n}\n","type":"registry:component"},{"path":"hooks/agents-ui/use-agent-audio-visualizer-aura.ts","content":"import { useEffect, useRef, useState, useCallback } from 'react';\nimport { type LocalAudioTrack, type RemoteAudioTrack } from 'livekit-client';\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';\n\nconst DEFAULT_SPEED = 10;\nconst DEFAULT_AMPLITUDE = 2;\nconst DEFAULT_FREQUENCY = 0.5;\nconst DEFAULT_SCALE = 0.2;\nconst DEFAULT_BRIGHTNESS = 1.5;\nconst DEFAULT_TRANSITION: ValueAnimationTransition = { duration: 0.5, ease: 'easeOut' };\nconst DEFAULT_PULSE_TRANSITION: ValueAnimationTransition = {\n  duration: 0.35,\n  ease: 'easeOut',\n  repeat: Infinity,\n  repeatType: 'mirror',\n};\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, motionValue, controls: controlsRef, animate: animateFn };\n}\n\nexport function useAgentAudioVisualizerAura(\n  state: AgentState | undefined,\n  audioTrack?: LocalAudioTrack | RemoteAudioTrack | TrackReferenceOrPlaceholder,\n  volumeProp?: number,\n) {\n  const [speed, setSpeed] = useState(DEFAULT_SPEED);\n  const {\n    value: scale,\n    animate: animateScale,\n    motionValue: scaleMotionValue,\n  } = useAnimatedValue(DEFAULT_SCALE);\n  const { value: amplitude, animate: animateAmplitude } = useAnimatedValue(DEFAULT_AMPLITUDE);\n  const { value: frequency, animate: animateFrequency } = useAnimatedValue(DEFAULT_FREQUENCY);\n  const { value: brightness, animate: animateBrightness } = useAnimatedValue(DEFAULT_BRIGHTNESS);\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 'idle':\n      case 'failed':\n      case 'disconnected':\n        setSpeed(10);\n        animateScale(0.2, DEFAULT_TRANSITION);\n        animateAmplitude(1.2, DEFAULT_TRANSITION);\n        animateFrequency(0.4, DEFAULT_TRANSITION);\n        animateBrightness(1.0, DEFAULT_TRANSITION);\n        return;\n      case 'listening':\n      case 'pre-connect-buffering':\n        setSpeed(20);\n        animateScale(0.3, { type: 'spring', duration: 1.0, bounce: 0.35 });\n        animateAmplitude(1.0, DEFAULT_TRANSITION);\n        animateFrequency(0.7, DEFAULT_TRANSITION);\n        animateBrightness([1.5, 2.0], DEFAULT_PULSE_TRANSITION);\n        return;\n      case 'thinking':\n      case 'connecting':\n      case 'initializing':\n        setSpeed(30);\n        animateScale(0.3, DEFAULT_TRANSITION);\n        animateAmplitude(0.5, DEFAULT_TRANSITION);\n        animateFrequency(1, DEFAULT_TRANSITION);\n        animateBrightness([0.5, 2.5], DEFAULT_PULSE_TRANSITION);\n        return;\n      case 'speaking':\n        setSpeed(70);\n        animateScale(0.3, DEFAULT_TRANSITION);\n        animateAmplitude(0.75, DEFAULT_TRANSITION);\n        animateFrequency(1.25, DEFAULT_TRANSITION);\n        animateBrightness(1.5, DEFAULT_TRANSITION);\n        return;\n    }\n  }, [state, animateScale, animateAmplitude, animateFrequency, animateBrightness]);\n\n  useEffect(() => {\n    if (state === 'speaking' && volume > 0 && !scaleMotionValue.isAnimating()) {\n      animateScale(0.2 + 0.2 * volume, { duration: 0 });\n    }\n  }, [\n    state,\n    volume,\n    scaleMotionValue,\n    animateScale,\n    animateAmplitude,\n    animateFrequency,\n    animateBrightness,\n  ]);\n\n  return {\n    speed,\n    scale,\n    amplitude,\n    frequency,\n    brightness,\n  };\n}\n","type":"registry:hook"}],"type":"registry:component"}