{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "elements-voice",
  "type": "registry:component",
  "title": "Voice Orb",
  "description": "The props-driven voice orb: five session states and four palettes, no runtime required.",
  "registryDependencies": [],
  "files": [
    {
      "type": "registry:component",
      "path": "components/assistant-ui/elements/voice.tsx",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { type FC, memo, useCallback, useEffect, useRef, useState } from \"react\";\n\nexport type VoiceOrbState =\n  | \"idle\"\n  | \"connecting\"\n  | \"listening\"\n  | \"speaking\"\n  | \"muted\";\n\nexport type VoiceOrbVariant = \"default\" | \"blue\" | \"violet\" | \"emerald\";\n\nconst VARIANT_COLORS: Record<VoiceOrbVariant, [number, number, number][]> = {\n  default: [\n    [0.55, 0.55, 0.6],\n    [0.7, 0.7, 0.75],\n    [0.4, 0.4, 0.45],\n  ],\n  blue: [\n    [0.2, 0.5, 1.0],\n    [0.4, 0.7, 1.0],\n    [0.1, 0.3, 0.8],\n  ],\n  violet: [\n    [0.6, 0.3, 1.0],\n    [0.8, 0.5, 1.0],\n    [0.4, 0.15, 0.8],\n  ],\n  emerald: [\n    [0.15, 0.75, 0.55],\n    [0.3, 0.9, 0.7],\n    [0.1, 0.55, 0.4],\n  ],\n};\n\ntype OrbParams = {\n  speed: number;\n  amplitude: number;\n  glow: number;\n  brightness: number;\n  pulse: number;\n  saturation: number;\n};\n\nconst STATE_PARAMS: Record<VoiceOrbState, OrbParams> = {\n  idle: {\n    speed: 0.15,\n    amplitude: 0.04,\n    glow: 0.15,\n    brightness: 0.55,\n    pulse: 0.0,\n    saturation: 0.7,\n  },\n  connecting: {\n    speed: 0.5,\n    amplitude: 0.1,\n    glow: 0.45,\n    brightness: 0.75,\n    pulse: 1.0,\n    saturation: 0.9,\n  },\n  listening: {\n    speed: 0.4,\n    amplitude: 0.14,\n    glow: 0.5,\n    brightness: 0.85,\n    pulse: 0.0,\n    saturation: 1.0,\n  },\n  speaking: {\n    speed: 1.4,\n    amplitude: 0.35,\n    glow: 0.9,\n    brightness: 1.0,\n    pulse: 0.0,\n    saturation: 1.0,\n  },\n  muted: {\n    speed: 0.06,\n    amplitude: 0.015,\n    glow: 0.08,\n    brightness: 0.35,\n    pulse: 0.0,\n    saturation: 0.2,\n  },\n};\n\nconst VERT_SRC = `#version 300 es\nin vec2 a_position;\nout vec2 v_uv;\nvoid main() {\n  v_uv = a_position * 0.5 + 0.5;\n  gl_Position = vec4(a_position, 0.0, 1.0);\n}`;\n\nconst FRAG_SRC = `#version 300 es\nprecision highp float;\n\nin vec2 v_uv;\nout vec4 fragColor;\n\nuniform float u_time;\nuniform float u_speed;\nuniform float u_amplitude;\nuniform float u_glow;\nuniform float u_brightness;\nuniform float u_pulse;\nuniform float u_saturation;\nuniform vec3 u_color0;\nuniform vec3 u_color1;\nuniform vec3 u_color2;\nuniform float u_dpr;\n\n// Simplex-like noise (3D)\nvec3 mod289(vec3 x) { return x - floor(x / 289.0) * 289.0; }\nvec4 mod289(vec4 x) { return x - floor(x / 289.0) * 289.0; }\nvec4 permute(vec4 x) { return mod289((x * 34.0 + 1.0) * x); }\nvec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }\n\nfloat snoise(vec3 v) {\n  const vec2 C = vec2(1.0 / 6.0, 1.0 / 3.0);\n  vec3 i = floor(v + dot(v, vec3(C.y)));\n  vec3 x0 = v - i + dot(i, vec3(C.x));\n  vec3 g = step(x0.yzx, x0.xyz);\n  vec3 l = 1.0 - g;\n  vec3 i1 = min(g, l.zxy);\n  vec3 i2 = max(g, l.zxy);\n  vec3 x1 = x0 - i1 + C.x;\n  vec3 x2 = x0 - i2 + C.y;\n  vec3 x3 = x0 - 0.5;\n  i = mod289(i);\n  vec4 p = permute(permute(permute(\n    i.z + vec4(0.0, i1.z, i2.z, 1.0))\n    + i.y + vec4(0.0, i1.y, i2.y, 1.0))\n    + i.x + vec4(0.0, i1.x, i2.x, 1.0));\n  vec4 j = p - 49.0 * floor(p / 49.0);\n  vec4 x_ = floor(j / 7.0);\n  vec4 y_ = floor(j - 7.0 * x_);\n  vec4 x = (x_ * 2.0 + 0.5) / 7.0 - 1.0;\n  vec4 y = (y_ * 2.0 + 0.5) / 7.0 - 1.0;\n  vec4 h = 1.0 - abs(x) - abs(y);\n  vec4 b0 = vec4(x.xy, y.xy);\n  vec4 b1 = vec4(x.zw, y.zw);\n  vec4 s0 = floor(b0) * 2.0 + 1.0;\n  vec4 s1 = floor(b1) * 2.0 + 1.0;\n  vec4 sh = -step(h, vec4(0.0));\n  vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy;\n  vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww;\n  vec3 g0 = vec3(a0.xy, h.x);\n  vec3 g1 = vec3(a0.zw, h.y);\n  vec3 g2 = vec3(a1.xy, h.z);\n  vec3 g3 = vec3(a1.zw, h.w);\n  vec4 norm = taylorInvSqrt(vec4(dot(g0,g0), dot(g1,g1), dot(g2,g2), dot(g3,g3)));\n  g0 *= norm.x; g1 *= norm.y; g2 *= norm.z; g3 *= norm.w;\n  vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);\n  m = m * m;\n  return 42.0 * dot(m * m, vec4(dot(g0,x0), dot(g1,x1), dot(g2,x2), dot(g3,x3)));\n}\n\nvoid main() {\n  vec2 uv = v_uv * 2.0 - 1.0;\n  float dist = length(uv);\n  float t = u_time * u_speed;\n\n  // Perfect circle — hard boundary, soft anti-aliased edge\n  float radius = 0.44;\n  float circle = 1.0 - smoothstep(radius - 0.008, radius + 0.008, dist);\n\n  if (circle < 0.001) {\n    // Outer glow only\n    float glowDist = dist - radius;\n    float glow = exp(-glowDist * 12.0) * u_glow * 0.4;\n    vec3 glowColor = mix(u_color0, u_color1, 0.5);\n    fragColor = vec4(glowColor * glow, glow);\n    return;\n  }\n\n  float n1 = snoise(vec3(uv * 2.0, t * 0.6)) * 0.5 + 0.5;\n  float n2 = snoise(vec3(uv * 3.5 + 7.0, t * 0.9)) * 0.5 + 0.5;\n  float n3 = snoise(vec3(uv * 1.5 - 3.0, t * 0.4 + 10.0)) * 0.5 + 0.5;\n\n  vec2 distort = vec2(\n    snoise(vec3(uv * 2.0 + 5.0, t * 0.7)),\n    snoise(vec3(uv * 2.0 + 15.0, t * 0.7))\n  ) * u_amplitude * 2.0;\n  float n4 = snoise(vec3((uv + distort) * 3.0, t * 0.5)) * 0.5 + 0.5;\n\n  vec3 col = mix(u_color0, u_color1, n1);\n  col = mix(col, u_color2, n2 * 0.5);\n  col = mix(col, u_color1 * 1.3, n4 * 0.4);\n\n  float vein = pow(n3, 3.0) * u_amplitude * 6.0;\n  col += vein * mix(u_color1, vec3(1.0), 0.3);\n\n  float centerDist = dist / radius;\n  float depthShade = 1.0 - centerDist * centerDist * 0.4;\n  col *= depthShade;\n\n  float rim = pow(centerDist, 4.0) * 0.6;\n  col += rim * mix(u_color0, vec3(1.0), 0.5);\n\n  vec2 lightPos = vec2(-0.15, -0.18);\n  float specDist = length(uv - lightPos);\n  float spec = exp(-specDist * specDist * 30.0) * 0.7;\n  col += spec * vec3(1.0);\n\n  vec2 lightPos2 = vec2(0.2, 0.25);\n  float spec2 = exp(-length(uv - lightPos2) * 8.0) * 0.15;\n  col += spec2 * u_color1;\n\n  float pulseFactor = 1.0 + u_pulse * sin(u_time * 3.5) * 0.35;\n\n  float lum = dot(col, vec3(0.299, 0.587, 0.114));\n  col = mix(vec3(lum), col, u_saturation);\n\n  col *= u_brightness * pulseFactor;\n\n  fragColor = vec4(col, circle);\n}`;\n\nfunction createShader(\n  gl: WebGL2RenderingContext,\n  type: number,\n  source: string,\n): WebGLShader | null {\n  const shader = gl.createShader(type);\n  if (!shader) return null;\n  gl.shaderSource(shader, source);\n  gl.compileShader(shader);\n  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n    gl.deleteShader(shader);\n    return null;\n  }\n  return shader;\n}\n\nfunction initWebGL(canvas: HTMLCanvasElement) {\n  const gl = canvas.getContext(\"webgl2\", {\n    alpha: true,\n    premultipliedAlpha: false,\n    antialias: true,\n  });\n  if (!gl) return null;\n\n  const vs = createShader(gl, gl.VERTEX_SHADER, VERT_SRC);\n  const fs = createShader(gl, gl.FRAGMENT_SHADER, FRAG_SRC);\n  if (!vs || !fs) return null;\n\n  const program = gl.createProgram()!;\n  gl.attachShader(program, vs);\n  gl.attachShader(program, fs);\n  gl.linkProgram(program);\n  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) return null;\n  gl.useProgram(program);\n\n  const buf = gl.createBuffer();\n  gl.bindBuffer(gl.ARRAY_BUFFER, buf);\n  gl.bufferData(\n    gl.ARRAY_BUFFER,\n    new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),\n    gl.STATIC_DRAW,\n  );\n  const loc = gl.getAttribLocation(program, \"a_position\");\n  gl.enableVertexAttribArray(loc);\n  gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);\n\n  gl.enable(gl.BLEND);\n  gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);\n\n  const uniforms = {\n    u_time: gl.getUniformLocation(program, \"u_time\"),\n    u_speed: gl.getUniformLocation(program, \"u_speed\"),\n    u_amplitude: gl.getUniformLocation(program, \"u_amplitude\"),\n    u_glow: gl.getUniformLocation(program, \"u_glow\"),\n    u_brightness: gl.getUniformLocation(program, \"u_brightness\"),\n    u_pulse: gl.getUniformLocation(program, \"u_pulse\"),\n    u_saturation: gl.getUniformLocation(program, \"u_saturation\"),\n    u_color0: gl.getUniformLocation(program, \"u_color0\"),\n    u_color1: gl.getUniformLocation(program, \"u_color1\"),\n    u_color2: gl.getUniformLocation(program, \"u_color2\"),\n    u_dpr: gl.getUniformLocation(program, \"u_dpr\"),\n  };\n\n  return { gl, uniforms };\n}\n\nfunction lerp(a: number, b: number, t: number) {\n  return a + (b - a) * t;\n}\nexport type VoiceOrbProps = {\n  state?: VoiceOrbState;\n  volume?: number;\n  variant?: VoiceOrbVariant;\n  className?: string;\n};\n\nexport const VoiceOrb: FC<VoiceOrbProps> = memo(\n  ({ state = \"idle\", volume = 0, variant = \"default\", className }) => {\n    const volumeRef = useRef(0);\n    volumeRef.current = volume;\n    const canvasRef = useRef<HTMLCanvasElement>(null);\n    const glRef = useRef<ReturnType<typeof initWebGL>>(null);\n    const animRef = useRef(0);\n    const startTime = useRef(performance.now());\n\n    const currentParams = useRef({ ...STATE_PARAMS.idle });\n    const targetParams = useRef({ ...STATE_PARAMS.idle });\n\n    useEffect(() => {\n      targetParams.current = { ...STATE_PARAMS[state] };\n    }, [state]);\n\n    const colors = VARIANT_COLORS[variant];\n\n    const [ready, setReady] = useState(false);\n    useEffect(() => {\n      const id = requestAnimationFrame(() => setReady(true));\n      return () => {\n        cancelAnimationFrame(id);\n        setReady(false);\n      };\n    }, []);\n\n    const render = useCallback(() => {\n      const ctx = glRef.current;\n      if (!ctx) return;\n      const { gl, uniforms } = ctx;\n      const canvas = canvasRef.current;\n      if (!canvas) return;\n\n      const p = currentParams.current;\n      const tp = targetParams.current;\n      const s = 0.045;\n      p.speed = lerp(p.speed, tp.speed, s);\n      p.amplitude = lerp(p.amplitude, tp.amplitude, s);\n      p.glow = lerp(p.glow, tp.glow, s);\n      p.brightness = lerp(p.brightness, tp.brightness, s);\n      p.pulse = lerp(p.pulse, tp.pulse, s);\n      p.saturation = lerp(p.saturation, tp.saturation, s);\n\n      const elapsed = (performance.now() - startTime.current) / 1000;\n\n      const dpr = window.devicePixelRatio || 1;\n      const rect = canvas.getBoundingClientRect();\n      const w = Math.round(rect.width * dpr);\n      const h = Math.round(rect.height * dpr);\n      if (canvas.width !== w || canvas.height !== h) {\n        canvas.width = w;\n        canvas.height = h;\n      }\n      gl.viewport(0, 0, w, h);\n\n      gl.clearColor(0, 0, 0, 0);\n      gl.clear(gl.COLOR_BUFFER_BIT);\n\n      const vol = volumeRef.current;\n\n      gl.uniform1f(uniforms.u_time, elapsed);\n      gl.uniform1f(uniforms.u_speed, p.speed + vol * 0.4);\n      gl.uniform1f(uniforms.u_amplitude, p.amplitude + vol * 0.12);\n      gl.uniform1f(uniforms.u_glow, p.glow + vol * 0.2);\n      gl.uniform1f(uniforms.u_brightness, p.brightness);\n      gl.uniform1f(uniforms.u_pulse, p.pulse);\n      gl.uniform1f(uniforms.u_saturation, p.saturation);\n      gl.uniform3fv(uniforms.u_color0, colors[0]!);\n      gl.uniform3fv(uniforms.u_color1, colors[1]!);\n      gl.uniform3fv(uniforms.u_color2, colors[2]!);\n      gl.uniform1f(uniforms.u_dpr, dpr);\n\n      gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);\n\n      animRef.current = requestAnimationFrame(render);\n    }, [colors]);\n\n    useEffect(() => {\n      if (!ready) return;\n      const canvas = canvasRef.current;\n      if (!canvas) return;\n\n      glRef.current = initWebGL(canvas);\n      if (!glRef.current) return;\n\n      animRef.current = requestAnimationFrame(render);\n\n      return () => {\n        cancelAnimationFrame(animRef.current);\n        const ctx = glRef.current;\n        if (ctx) {\n          const ext = ctx.gl.getExtension(\"WEBGL_lose_context\");\n          ext?.loseContext();\n        }\n        glRef.current = null;\n      };\n    }, [ready, render]);\n\n    return (\n      <canvas\n        ref={canvasRef}\n        className={cn(\"aui-voice-orb size-16 shrink-0\", className)}\n        data-state={state}\n      />\n    );\n  },\n);\n\nVoiceOrb.displayName = \"VoiceOrb\";\n"
    }
  ]
}