{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "agent-orb",
  "type": "registry:ui",
  "title": "Agent Orb",
  "description": "Calm, softly glassy voice-agent sphere drawn in a shader: slow colour clouds that breathe when idle, swell with audio and turn while thinking.",
  "dependencies": [],
  "registryDependencies": [
    "https://kuro.abstergo.fyi/r/kura-theme.json",
    "https://kuro.abstergo.fyi/r/kura-merge-refs.json",
    "https://kuro.abstergo.fyi/r/kura-shader-color.json",
    "https://kuro.abstergo.fyi/r/shader-canvas.json",
    "https://kuro.abstergo.fyi/r/use-audio-analyser.json",
    "https://kuro.abstergo.fyi/r/use-reduced-motion.json",
    "utils"
  ],
  "files": [
    {
      "path": "registry/kura/agent-orb/agent-orb.glsl.ts",
      "type": "registry:ui",
      "target": "components/kura/agent-orb/agent-orb.glsl.ts",
      "content": "import { GLSL_COLOR } from \"@/lib/kura/shader-color\";\n\n/**\n * The agent orb: an analytic sphere (no geometry) holding slow, soft clouds of colour. Per pixel we\n *   1. find the (breathing, audio-swollen) silhouette and the sphere normal,\n *   2. sample two octaves of domain-warped 3D simplex noise *on the sphere surface*, turning\n *      slowly like a globe and twisting around the view axis while thinking — so the clouds wrap\n *      around the curvature instead of sliding across a disc,\n *   3. blend the palette in OKLab (pearl → mid → signal tone), no muddy midpoints,\n *   4. light it quietly: a little wrap diffuse, a pearly fresnel rim, one broad window sheen,\n *   5. outside the sphere: a soft contact shadow on light backgrounds, a faint halo on dark ones,\n *      written as premultiplied alpha so it sits on any surface.\n * One sRGB encode + IGN dither at the end.\n */\nexport const AGENT_ORB_FRAG = /* glsl */ `\nuniform vec3 u_deep;    // OKLab: the signal tone (densest clouds)\nuniform vec3 u_mid;     // OKLab: body\nuniform vec3 u_bright;  // OKLab: the light between clouds\nuniform vec3 u_glint;   // OKLab: rim and sheen\nuniform float u_energy;  // 0–1 inner brightness and turbulence\nuniform float u_swirl;   // 0–1 vortex strength\nuniform float u_spin;    // integrated vortex angle\nuniform float u_phase;   // integrated flow time\nuniform float u_level;   // smoothed audio level 0–1\nuniform float u_breath;  // 0–1 idle breathing amplitude\nuniform float u_halo;    // 0–1 outer halo\nuniform float u_light;   // 1 on light backgrounds, 0 on dark\n\n${GLSL_COLOR}\n\nvec4 permute4(vec4 x) { return mod(((x * 34.0) + 1.0) * x, 289.0); }\nvec4 taylorInvSqrt(vec4 r) { return 1.79284291400159 - 0.85373472095314 * r; }\nfloat snoise(vec3 v) {\n  const vec2 C = vec2(1.0 / 6.0, 1.0 / 3.0);\n  const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);\n  vec3 i = floor(v + dot(v, C.yyy));\n  vec3 x0 = v - i + dot(i, C.xxx);\n  vec3 g = step(x0.yzx, x0.xyz);\n  vec3 l = 1.0 - g;\n  vec3 i1 = min(g.xyz, l.zxy);\n  vec3 i2 = max(g.xyz, l.zxy);\n  vec3 x1 = x0 - i1 + C.xxx;\n  vec3 x2 = x0 - i2 + C.yyy;\n  vec3 x3 = x0 - D.yyy;\n  i = mod(i, 289.0);\n  vec4 p = permute4(permute4(permute4(\n    i.z + vec4(0.0, i1.z, i2.z, 1.0)) + i.y + vec4(0.0, i1.y, i2.y, 1.0)) + i.x + vec4(0.0, i1.x, i2.x, 1.0));\n  float n_ = 1.0 / 7.0;\n  vec3 ns = n_ * D.wyz - D.xzx;\n  vec4 j = p - 49.0 * floor(p * ns.z * ns.z);\n  vec4 x_ = floor(j * ns.z);\n  vec4 y_ = floor(j - 7.0 * x_);\n  vec4 x = x_ * ns.x + ns.yyyy;\n  vec4 y = y_ * ns.x + ns.yyyy;\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 p0 = vec3(a0.xy, h.x);\n  vec3 p1 = vec3(a0.zw, h.y);\n  vec3 p2 = vec3(a1.xy, h.z);\n  vec3 p3 = vec3(a1.zw, h.w);\n  vec4 norm = taylorInvSqrt(vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3)));\n  p0 *= norm.x; p1 *= norm.y; p2 *= norm.z; p3 *= 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(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3)));\n}\n\nmat2 rot(float a) { float c = cos(a), s = sin(a); return mat2(c, -s, s, c); }\n\nvoid main() {\n  float s = min(u_resolution.x, u_resolution.y);\n  float px = 2.0 / s;\n  vec2 p = (gl_FragCoord.xy - 0.5 * u_resolution) * px;\n  float t = u_phase;\n\n  // Silhouette: a slow breath when idle, a gentle swell with the voice.\n  float R = 0.84 * (1.0 + 0.012 * u_breath * sin(u_time * 1.3) + 0.04 * u_level);\n  float r = length(p);\n  vec2 dir = r > 0.0 ? p / r : vec2(0.0, 1.0);\n  float edge = R * (1.0 + snoise(vec3(dir * 1.1, t * 0.6)) * 0.012 * u_level);\n  float mask = smoothstep(edge + px, edge - px, r);\n\n  vec3 lab = u_mid;\n  if (mask > 0.0) {\n    vec2 q = p / edge;\n    float rq = min(length(q), 1.0);\n    float z = sqrt(max(0.0, 1.0 - rq * rq));\n    vec3 n = normalize(vec3(q, z));\n\n    // Clouds live on the surface: turn the sphere slowly, twist it while thinking.\n    vec3 P = n;\n    P.xy = rot(u_spin + u_swirl * 1.8 * (1.0 - rq)) * P.xy;\n    P.xz = rot(t * 0.16) * P.xz;\n    vec3 w = P * 0.72;\n    float n1 = snoise(w + vec3(0.0, t * 0.17, t * 0.1));\n    float n2 = snoise(w * 1.45 + n1 * (0.45 + 0.6 * u_energy) + vec3(t * 0.12, 3.1, -t * 0.08));\n    float clouds = smoothstep(-0.75, 0.85, n1 * 0.7 + n2 * 0.38);\n    float gaps = smoothstep(0.05, 0.95, snoise(w * 0.8 - vec3(2.7, t * 0.15, 1.3) + n2 * 0.25));\n\n    vec3 body = mix(u_bright, u_mid, clouds);\n    body = mix(body, u_deep, smoothstep(0.5, 1.0, clouds) * (0.62 + 0.3 * u_energy));\n    body = mix(body, u_bright, gaps * (0.22 + 0.3 * u_level));\n\n    // Volume: soft wrap diffuse from the top left, slightly deeper toward the limb.\n    vec3 L = normalize(vec3(-0.45, 0.6, 0.66));\n    float diff = dot(n, L) * 0.5 + 0.5;\n    body.x *= mix(0.86, 1.03, diff) * (1.0 - 0.08 * pow(1.0 - z, 1.5));\n    // The voice lifts the core a touch.\n    body.x += exp(-dot(q, q) * 2.4) * 0.05 * u_level;\n\n    // Glass: a pearly fresnel rim, one broad window sheen, a small soft highlight.\n    float fres = pow(1.0 - z, 3.0);\n    body = mix(body, u_glint, fres * mix(0.35, 0.55, u_light));\n    vec3 H = normalize(vec3(-0.5, 0.62, 0.6));\n    float nh = max(dot(n, H), 0.0);\n    body = mix(body, u_glint, smoothstep(0.84, 0.99, nh) * 0.2 + pow(nh, 80.0) * 0.3);\n\n    // A hairline just inside the edge keeps a pale orb crisp on a pale page.\n    body.x -= 0.045 * u_light * smoothstep(edge - px * 2.5, edge - px * 0.5, r);\n    lab = body;\n  }\n\n  // Outside: contact shadow on light backgrounds, a faint halo on dark ones.\n  float outside = max(r - edge, 0.0);\n  float sd = max(length(p - vec2(0.0, -0.04)) - edge, 0.0);\n  // Both fade to zero well inside the square box, so no edge ever shows.\n  float room = 1.0 - edge;\n  float shadowA = u_light * exp(-sd * 18.0) * 0.1 * smoothstep(room, room * 0.3, sd) * (1.0 - mask);\n  float haloA = (1.0 - u_light) * u_halo * exp(-outside * 14.0) * 0.22\n              * smoothstep(room, room * 0.2, outside) * (1.0 - mask);\n  vec3 orb = linearToSrgb(oklabToLinear(lab));\n  vec3 haloC = linearToSrgb(oklabToLinear(u_deep));\n  float a = mask + haloA + shadowA;\n  vec3 premul = orb * mask + haloC * haloA;\n  premul += (ign(gl_FragCoord.xy) - 0.5) / 255.0 * a;\n  fragColor = vec4(premul, a);\n}\n`;\n"
    },
    {
      "path": "registry/kura/agent-orb/agent-orb.tsx",
      "type": "registry:ui",
      "target": "components/kura/agent-orb/agent-orb.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport type { AudioAnalyser } from \"@/hooks/kura/use-audio-analyser\";\nimport { useReducedMotion } from \"@/hooks/kura/use-reduced-motion\";\nimport { cn } from \"@/lib/utils\";\nimport { mergeRefs } from \"@/lib/kura/merge-refs\";\nimport { useResolvedColors, type Vec3 } from \"@/lib/kura/shader-color\";\nimport { ShaderCanvas } from \"@/components/kura/shader-canvas\";\nimport { AGENT_ORB_FRAG } from \"./agent-orb.glsl\";\n\nexport type AgentOrbState = \"idle\" | \"listening\" | \"thinking\" | \"speaking\" | \"error\";\nexport type AgentOrbPreset = \"signal\" | \"pearl\" | \"graphite\" | \"ocean\" | \"amber\";\n\n/**\n * Palettes as [deep, mid, bright, glint]: the densest clouds, the body, the light between the\n * clouds, and the rim/sheen. `signal` is built from Kura tokens and follows the theme — pearl with\n * blue clouds on light pages, graphite with blue clouds on dark ones. The rest are fixed.\n */\nexport const agentOrbPresets: Record<AgentOrbPreset, readonly [string, string, string, string]> = {\n  signal: [\n    \"var(--kura-accent)\",\n    \"color-mix(in oklab, var(--kura-accent) 38%, var(--kura-surface))\",\n    \"color-mix(in oklab, var(--kura-accent) 9%, var(--kura-surface))\",\n    \"oklch(0.99 0.004 255)\",\n  ],\n  pearl: [\n    \"oklch(0.7 0.008 255)\",\n    \"oklch(0.85 0.006 255)\",\n    \"oklch(0.975 0.003 255)\",\n    \"oklch(1 0 0)\",\n  ],\n  graphite: [\n    \"oklch(0.2 0.004 255)\",\n    \"oklch(0.33 0.008 255)\",\n    \"oklch(0.56 0.02 255)\",\n    \"oklch(0.9 0.01 255)\",\n  ],\n  ocean: [\n    \"oklch(0.4 0.16 264)\",\n    \"oklch(0.57 0.17 258)\",\n    \"oklch(0.82 0.08 245)\",\n    \"oklch(0.97 0.02 240)\",\n  ],\n  amber: [\"oklch(0.7 0.12 58)\", \"oklch(0.85 0.06 70)\", \"oklch(0.97 0.015 85)\", \"oklch(1 0 0)\"],\n};\n\n/** Error follows the theme like `signal`, with the danger token as the cloud colour. */\nconst ERROR_PALETTE = [\n  \"var(--kura-danger)\",\n  \"color-mix(in oklab, var(--kura-danger) 40%, var(--kura-surface))\",\n  \"color-mix(in oklab, var(--kura-danger) 6%, var(--kura-surface))\",\n  \"oklch(0.99 0.004 25)\",\n] as const;\n\nconst STATE_LABEL: Record<AgentOrbState, string> = {\n  idle: \"Voice agent idle\",\n  listening: \"Listening\",\n  thinking: \"Thinking\",\n  speaking: \"Speaking\",\n  error: \"Voice agent error\",\n};\n\ninterface Targets {\n  energy: number;\n  swirl: number;\n  spinSpeed: number;\n  flow: number;\n  breath: number;\n  halo: number;\n  error: number;\n}\n\nconst TARGETS: Record<AgentOrbState, Targets> = {\n  idle: { energy: 0.15, swirl: 0.05, spinSpeed: 0.04, flow: 0.2, breath: 1, halo: 0.2, error: 0 },\n  listening: {\n    energy: 0.4,\n    swirl: 0.08,\n    spinSpeed: 0.08,\n    flow: 0.4,\n    breath: 0.4,\n    halo: 0.35,\n    error: 0,\n  },\n  thinking: {\n    energy: 0.5,\n    swirl: 0.6,\n    spinSpeed: 0.7,\n    flow: 0.5,\n    breath: 0.3,\n    halo: 0.35,\n    error: 0,\n  },\n  speaking: {\n    energy: 0.5,\n    swirl: 0.12,\n    spinSpeed: 0.14,\n    flow: 0.5,\n    breath: 0.2,\n    halo: 0.55,\n    error: 0,\n  },\n  error: { energy: 0.1, swirl: 0, spinSpeed: 0.02, flow: 0.12, breath: 0.6, halo: 0.25, error: 1 },\n};\n\nexport interface AgentOrbProps extends Omit<React.ComponentProps<\"div\">, \"children\"> {\n  /** What the agent is doing. Drives motion, colour and the announced label. Default \"idle\". */\n  state?: AgentOrbState;\n  /** A preset, or four CSS colours `[deep, mid, bright, glint]`. Default \"signal\". */\n  colors?: AgentOrbPreset | readonly [string, string, string, string];\n  /**\n   * Audio analyser (from `useAudioAnalyser`). Its level drives the orb while `listening` (input)\n   * or `speaking` (output). Overridden by `inputLevel`/`outputLevel` when those are set.\n   */\n  analyser?: AudioAnalyser;\n  /** Microphone level 0–1, used while `listening`. */\n  inputLevel?: number;\n  /** Agent voice level 0–1, used while `speaking`. */\n  outputLevel?: number;\n  /** Accessible text for the current state. Default: \"Listening\", \"Thinking\", … */\n  label?: string;\n  /** Announce state changes politely to screen readers. Default true. */\n  announce?: boolean;\n  /** Device-pixel-ratio cap. Default 2. */\n  maxDpr?: number;\n}\n\nfunction lerp(a: number, b: number, k: number) {\n  return a + (b - a) * k;\n}\n\n/**\n * The voice-agent orb: a calm, softly glassy sphere of slow colour clouds, rendered in a fragment\n * shader. It breathes when idle, swells with the microphone while listening, turns while thinking,\n * lifts with the agent's voice while speaking and shifts to the danger colour on error — every\n * transition is eased on the GPU uniforms. Decorative canvas plus a polite text equivalent.\n */\nexport function AgentOrb({\n  state = \"idle\",\n  colors = \"signal\",\n  analyser,\n  inputLevel,\n  outputLevel,\n  label,\n  announce = true,\n  maxDpr = 2,\n  className,\n  style,\n  ref,\n  ...props\n}: AgentOrbProps) {\n  const rootRef = React.useRef<HTMLDivElement>(null);\n  const reducedMotion = useReducedMotion();\n  const palette = typeof colors === \"string\" ? agentOrbPresets[colors] : colors;\n  const list = React.useMemo(() => [...palette, ...ERROR_PALETTE, \"var(--kura-canvas)\"], [palette]);\n  const { colors: resolved, version } = useResolvedColors(rootRef, list);\n\n  // Everything the render loop needs, without re-creating uniforms on every prop change.\n  const live = React.useRef({ state, analyser, inputLevel, outputLevel, reducedMotion });\n  live.current = { state, analyser, inputLevel, outputLevel, reducedMotion };\n\n  // Animated uniform state, eased toward the targets of the current `state` every frame.\n  const sim = React.useRef({\n    ...TARGETS[state],\n    level: 0,\n    spin: 0,\n    phase: 0,\n    last: 0,\n    out: {\n      deep: [0, 0, 0] as Vec3,\n      mid: [0, 0, 0] as Vec3,\n      bright: [0, 0, 0] as Vec3,\n      glint: [0, 0, 0] as Vec3,\n    },\n    light: 0,\n  });\n\n  // Under reduced motion the shader is frozen in time; wake it briefly to fade between states.\n  const [awake, setAwake] = React.useState(true);\n  // biome-ignore lint/correctness/useExhaustiveDependencies: wake on state/theme/palette changes.\n  React.useEffect(() => {\n    if (!reducedMotion) return;\n    setAwake(true);\n    const id = setTimeout(() => setAwake(false), 900);\n    return () => clearTimeout(id);\n  }, [reducedMotion, state, version, list]);\n\n  const uniforms = React.useMemo(() => {\n    const tick = () => {\n      const s = sim.current;\n      const now = performance.now();\n      const dt = s.last ? Math.min(0.1, (now - s.last) / 1000) : 1;\n      s.last = now;\n      const {\n        state: st,\n        analyser: an,\n        inputLevel: inL,\n        outputLevel: outL,\n        reducedMotion: rm,\n      } = live.current;\n      const target = TARGETS[st];\n      const k = 1 - Math.exp(-dt / 0.35); // ~350ms ease, retargets mid-flight\n      s.energy = lerp(s.energy, target.energy, k);\n      s.swirl = lerp(s.swirl, target.swirl, k);\n      s.spinSpeed = lerp(s.spinSpeed, target.spinSpeed, k);\n      s.flow = lerp(s.flow, target.flow, k);\n      s.breath = lerp(s.breath, target.breath, k);\n      s.halo = lerp(s.halo, target.halo, k);\n      s.error = lerp(s.error, target.error, 1 - Math.exp(-dt / 0.25));\n\n      let level = 0;\n      if (!rm && (st === \"listening\" || st === \"speaking\")) {\n        const manual = st === \"listening\" ? inL : outL;\n        level = manual ?? an?.read().level ?? 0;\n        level = Math.min(1, Math.max(0, level));\n      }\n      // Fast attack, slower release, like a VU meter.\n      s.level = lerp(s.level, level, 1 - Math.exp(-dt / (level > s.level ? 0.05 : 0.18)));\n      if (!rm) {\n        s.spin += dt * s.spinSpeed * 1.6;\n        s.phase += dt * s.flow;\n      }\n\n      // Blend toward the error palette in OKLab; the shader works in OKLab too.\n      const c = resolved.current;\n      const mix = (i: number, out: Vec3) => {\n        const a = c[i]?.oklab ?? [0.6, 0, 0];\n        const b = c[i + 4]?.oklab ?? a;\n        for (let j = 0; j < 3; j++) out[j] = lerp(a[j]!, b[j]!, s.error);\n        return out;\n      };\n      mix(0, s.out.deep);\n      mix(1, s.out.mid);\n      mix(2, s.out.bright);\n      mix(3, s.out.glint);\n      s.light = (c[8]?.oklab[0] ?? 1) > 0.55 ? 1 : 0;\n      return s.out.deep;\n    };\n    const s = sim.current;\n    // ShaderCanvas evaluates uniforms in insertion order: `u_deep` runs the tick first.\n    return {\n      u_deep: tick,\n      u_mid: () => s.out.mid,\n      u_bright: () => s.out.bright,\n      u_glint: () => s.out.glint,\n      u_energy: () => s.energy + s.level * 0.5,\n      u_swirl: () => s.swirl,\n      u_spin: () => s.spin,\n      u_phase: () => s.phase,\n      u_level: () => s.level,\n      u_breath: () => s.breath,\n      u_halo: () => s.halo + s.level * 0.6,\n      u_light: () => s.light,\n    };\n  }, [resolved]);\n\n  const text = label ?? STATE_LABEL[state];\n  const [deep, mid, bright, glint] = state === \"error\" ? ERROR_PALETTE : palette;\n\n  return (\n    <div\n      ref={mergeRefs(rootRef, ref)}\n      data-slot=\"agent-orb\"\n      data-state={state}\n      className={cn(\"relative isolate aspect-square\", className)}\n      style={style}\n      {...props}\n    >\n      <ShaderCanvas\n        data-slot=\"agent-orb-canvas\"\n        fragmentShader={AGENT_ORB_FRAG}\n        uniforms={uniforms}\n        respectReducedMotion={false}\n        speed={reducedMotion ? 0 : 1}\n        paused={reducedMotion && !awake}\n        maxDpr={maxDpr}\n        className=\"absolute inset-0\"\n        fallback={\n          <div\n            aria-hidden\n            data-slot=\"agent-orb-fallback\"\n            className={cn(\n              \"absolute inset-[8%] overflow-hidden rounded-full shadow-kura-md transition-[scale] duration-(--kura-duration-slower) ease-kura-out motion-reduce:transition-none\",\n              state === \"speaking\" && \"scale-[1.03] motion-reduce:scale-100\",\n            )}\n            style={{ background: bright }}\n          >\n            <div\n              className={cn(\n                \"absolute inset-0 rounded-full blur-md\",\n                state === \"thinking\" &&\n                  \"animate-kura-spin [animation-duration:3.2s] motion-reduce:animate-none\",\n              )}\n              style={{\n                background: `radial-gradient(60% 55% at 70% 72%, ${deep}, transparent 70%), radial-gradient(55% 50% at 28% 62%, ${mid}, transparent 72%), radial-gradient(50% 45% at 60% 22%, ${mid}, transparent 70%)`,\n              }}\n            />\n            <div\n              className=\"absolute inset-0 rounded-full\"\n              style={{\n                background: `radial-gradient(42% 32% at 34% 24%, color-mix(in oklab, ${glint} 55%, transparent), transparent 70%), radial-gradient(circle at 50% 50%, transparent 66%, color-mix(in oklab, ${glint} 45%, transparent) 100%)`,\n              }}\n            />\n          </div>\n        }\n      />\n      <span className=\"sr-only\" {...(announce ? { role: \"status\", \"aria-live\": \"polite\" } : {})}>\n        {text}\n      </span>\n    </div>\n  );\n}\n"
    },
    {
      "path": "registry/kura/agent-orb/index.ts",
      "type": "registry:ui",
      "target": "components/kura/agent-orb/index.ts",
      "content": "export { AgentOrb, type AgentOrbProps } from \"./agent-orb\";\n"
    }
  ],
  "categories": [
    "voice",
    "voice",
    "orb",
    "agent",
    "audio"
  ],
  "docs": "Docs: https://kuro.abstergo.fyi/components/agent-orb · Markdown for agents: https://kuro.abstergo.fyi/components/agent-orb.md",
  "meta": {
    "kura": {
      "status": "beta",
      "whenToUse": [
        "The central visual of a voice agent or call screen: idle, listening, thinking, speaking, error.",
        "Showing that an assistant is hearing the user (mic level) or talking back (TTS level).",
        "A 'talk to the agent' entry point inside a button or a card.",
        "An ambient, branded presence for an AI assistant in an empty state."
      ],
      "whenNotToUse": [
        "Precise audio feedback such as a recording meter or waveform — use audio-waveform or voice-dictation.",
        "Text chat 'thinking' feedback inside a message list — use thinking-indicator or shimmer-text.",
        "More than one per view: the orb is the accent element; repeat it and it stops meaning anything."
      ]
    }
  }
}
