{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "agent-orb",
  "type": "registry:ui",
  "title": "Agent Orb",
  "description": "Glassy liquid sphere for voice agents, drawn in a shader: breathes when idle, reacts to mic and voice levels, swirls 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) shaded like thick glass holding a luminous\n * liquid. Per pixel we\n *   1. find the (audio-deformed) silhouette and the sphere normal,\n *   2. refract the view ray and march 5 steps through a domain-warped 3D noise volume — the\n *      \"liquid\", twisted around the view axis for the thinking swirl,\n *   3. add ridged-noise caustic filaments where light would focus (opposite the key light),\n *   4. light it: wrap diffuse, a fresnel rim, a soft window reflection and a tight specular,\n *   5. composite a faint halo outside the sphere, in premultiplied alpha.\n * All lighting happens in linear RGB; one sRGB encode + IGN dither at the end.\n */\nexport const AGENT_ORB_FRAG = /* glsl */ `\nuniform vec3 u_deep;\nuniform vec3 u_mid;\nuniform vec3 u_bright;\nuniform vec3 u_glint;\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\nfloat fbm(vec3 p) {\n  float v = 0.0, a = 0.5;\n  for (int i = 0; i < 3; i++) { v += a * snoise(p); p = p * 2.03 + 11.7; a *= 0.5; }\n  return v;\n}\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: breathing + audio-driven wobble along the rim.\n  float R = 0.7 * (1.0 + 0.022 * u_breath * sin(u_time * 1.55) + 0.075 * u_level);\n  float r = length(p);\n  vec2 dir = r > 0.0 ? p / r : vec2(0.0, 1.0);\n  float wob = snoise(vec3(dir * 1.2, t * 0.55)) * (0.006 + 0.03 * u_level)\n            + snoise(vec3(dir * 2.6, t * 1.4 + 3.0)) * 0.014 * u_level;\n  float edge = R * (1.0 + wob);\n  float mask = smoothstep(edge + px * 1.2, edge - px * 1.2, r);\n\n  vec3 col = vec3(0.0);\n  if (mask > 0.0) {\n    vec2 q = p / edge;\n    float z = sqrt(max(0.0, 1.0 - dot(q, q)));\n    vec3 n = normalize(vec3(q, z));\n    vec3 L = normalize(vec3(-0.55, 0.65, 0.55));\n\n    // March the refracted ray through the liquid: large, slow masses of colour (deep ↔ mid),\n    // lit from within where the density is high.\n    vec3 rd = refract(vec3(0.0, 0.0, -1.0), n, 0.72);\n    vec3 ro = vec3(q, z);\n    float chord = 1.5 * z + 0.3;\n    float dens = 0.0;\n    float rib = 0.0;\n    for (int k = 0; k < 5; k++) {\n      float fk = (float(k) + 0.5) / 5.0;\n      vec3 P = ro + rd * chord * fk;\n      // Vortex: twist around the view axis, faster toward the core.\n      float rr = length(P.xy);\n      P.xy = rot(u_spin * (1.0 - 0.5 * rr) + u_swirl * 2.4 * (1.0 - rr)) * P.xy;\n      vec3 w = P * 0.95 + vec3(0.0, -t * 0.18, t * 0.3);\n      vec3 warp = vec3(snoise(w * 0.7 + t * 0.12), snoise(w * 0.7 + 7.3), snoise(w * 0.7 - 4.1 - t * 0.1));\n      float d = fbm(w + warp * (0.6 + 0.6 * u_energy));\n      dens += smoothstep(-0.3, 0.45, d) * (1.0 - fk * 0.35);\n      // Thin luminous threads that appear with energy.\n      rib += pow(1.0 - abs(snoise(w * 1.3 + warp)), 6.0) * (1.0 - fk * 0.6);\n    }\n    dens /= 4.1;\n    rib /= 3.8;\n\n    // Volume: light travels further through the edges, so they absorb more.\n    float thickness = sqrt(z);\n    vec3 body = mix(u_deep, u_mid, dens);\n    body = mix(body, u_bright, smoothstep(0.55, 1.0, dens) * (0.35 + 0.45 * u_energy));\n    float diff = clamp(dot(n, L) * 0.5 + 0.5, 0.0, 1.0);\n    col = body * mix(0.5, 1.12, diff) * mix(0.72, 1.0, thickness);\n\n    // Core: a soft inner light, offset away from the key light, swelling with the voice.\n    vec2 coreP = q - vec2(0.1, -0.16);\n    float core = exp(-dot(coreP, coreP) * (3.2 - 1.2 * u_level));\n    col += u_bright * core * (0.12 + 0.35 * u_energy + 0.5 * u_level);\n    col += u_bright * rib * (0.03 + 0.22 * u_energy * u_energy + 0.25 * u_level);\n\n    // Caustics: soft pools of focused light drifting opposite the key light.\n    vec2 cq = rot(u_spin * 0.35) * q;\n    float cn = snoise(vec3(cq * 2.6, t * 0.35)) * 0.6 + snoise(vec3(cq * 5.2 + 3.0, t * 0.5)) * 0.4;\n    float caus = smoothstep(0.25, 0.85, cn);\n    float focus = smoothstep(0.05, 0.85, dot(q, -normalize(L.xy))) * smoothstep(1.0, 0.7, length(q));\n    col += mix(u_bright, u_glint, 0.35) * caus * focus * (0.28 + 0.55 * u_energy + 0.4 * u_level);\n\n    // Subsurface: light pooled along the lower inner edge.\n    float sss = pow(1.0 - z, 1.4) * smoothstep(0.3, -0.9, q.y + q.x * 0.35);\n    col += u_bright * sss * (0.4 + 0.35 * u_energy);\n\n    // Glass: a coloured fresnel rim, a bright crescent on the lit side, a crisp specular.\n    float fres = pow(1.0 - z, 3.0);\n    col = mix(col, u_bright * 0.85, fres * 0.45);\n    float lit = smoothstep(0.1, 0.95, dot(normalize(q + 1e-4), normalize(L.xy)));\n    float crescent = pow(1.0 - z, 2.2) * lit * smoothstep(0.995, 0.93, length(q));\n    col += u_glint * crescent * 0.55;\n    vec3 H = normalize(vec3(-0.42, 0.52, 0.74));\n    float window = smoothstep(0.955, 0.99, dot(n, H));\n    col += u_glint * window * 0.12;\n    float spec = pow(max(dot(reflect(-L, n), vec3(0.0, 0.0, 1.0)), 0.0), 120.0);\n    col += u_glint * spec * 1.2;\n  }\n\n  // Halo: soft falloff outside the rim, stronger while speaking.\n  float outside = max(r - edge, 0.0);\n  float haloA = u_halo * exp(-outside * 10.0) * smoothstep(0.32, 0.05, outside) * (1.0 - mask) * (0.5 - 0.2 * u_light);\n  vec3 orb = linearToSrgb(col);\n  vec3 haloC = linearToSrgb(u_bright);\n  float a = mask + haloA;\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 = \"jade\" | \"ocean\" | \"amber\" | \"graphite\";\n\n/** Palettes as [deep, mid, bright, glint] OKLCH colours. */\nexport const agentOrbPresets: Record<AgentOrbPreset, readonly [string, string, string, string]> = {\n  jade: [\n    \"oklch(0.3 0.06 188)\",\n    \"oklch(0.56 0.115 174)\",\n    \"oklch(0.84 0.13 166)\",\n    \"oklch(0.98 0.03 150)\",\n  ],\n  ocean: [\n    \"oklch(0.27 0.07 255)\",\n    \"oklch(0.53 0.13 242)\",\n    \"oklch(0.82 0.1 218)\",\n    \"oklch(0.98 0.02 210)\",\n  ],\n  amber: [\n    \"oklch(0.33 0.08 42)\",\n    \"oklch(0.64 0.15 55)\",\n    \"oklch(0.87 0.13 82)\",\n    \"oklch(0.99 0.03 95)\",\n  ],\n  graphite: [\n    \"oklch(0.22 0.006 85)\",\n    \"oklch(0.46 0.01 85)\",\n    \"oklch(0.82 0.01 85)\",\n    \"oklch(0.99 0 0)\",\n  ],\n};\n\nconst ERROR_PALETTE = [\n  \"oklch(0.3 0.08 25)\",\n  \"oklch(0.55 0.17 25)\",\n  \"oklch(0.78 0.13 35)\",\n  \"oklch(0.97 0.03 45)\",\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.2, swirl: 0.05, spinSpeed: 0.05, flow: 0.22, breath: 1, halo: 0.25, error: 0 },\n  listening: {\n    energy: 0.45,\n    swirl: 0.1,\n    spinSpeed: 0.1,\n    flow: 0.45,\n    breath: 0.4,\n    halo: 0.45,\n    error: 0,\n  },\n  thinking: {\n    energy: 0.6,\n    swirl: 0.85,\n    spinSpeed: 1.1,\n    flow: 0.6,\n    breath: 0.3,\n    halo: 0.4,\n    error: 0,\n  },\n  speaking: {\n    energy: 0.55,\n    swirl: 0.15,\n    spinSpeed: 0.18,\n    flow: 0.55,\n    breath: 0.2,\n    halo: 0.7,\n    error: 0,\n  },\n  error: { energy: 0.15, swirl: 0, spinSpeed: 0.02, flow: 0.12, breath: 0.6, halo: 0.3, 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 \"jade\". */\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 glassy, liquid sphere rendered in a fragment shader. It breathes when\n * idle, ripples with the microphone while listening, swirls while thinking, pulses with the\n * agent's voice while speaking and turns red on error — every transition is smoothed on the GPU\n * uniforms. Decorative canvas plus a polite text equivalent of the state.\n */\nexport function AgentOrb({\n  state = \"idle\",\n  colors = \"jade\",\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      const c = resolved.current;\n      const mix = (i: number, out: Vec3) => {\n        const a = c[i]?.linear ?? [0.3, 0.3, 0.3];\n        const b = c[i + 4]?.linear ?? 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-[15%] overflow-hidden rounded-full transition-[scale] duration-(--kura-duration-slower) ease-kura-out motion-reduce:transition-none\",\n              state === \"speaking\" && \"scale-[1.04] motion-reduce:scale-100\",\n            )}\n            style={{\n              background: mid,\n              boxShadow: `0 0 48px -8px color-mix(in oklab, ${bright} 45%, transparent)`,\n            }}\n          >\n            <div\n              className={cn(\n                \"absolute inset-0 rounded-full\",\n                state === \"thinking\" &&\n                  \"animate-kura-spin [animation-duration:2.4s] motion-reduce:animate-none\",\n              )}\n              style={{\n                background: `conic-gradient(from 200deg, ${mid}, ${deep}, ${mid}, ${bright}, ${mid})`,\n              }}\n            />\n            <div\n              className=\"absolute inset-0 rounded-full\"\n              style={{\n                background: `radial-gradient(circle at 34% 28%, ${glint} 0%, transparent 16%), radial-gradient(circle at 60% 82%, color-mix(in oklab, ${bright} 70%, transparent) 0%, transparent 42%), radial-gradient(circle at 50% 50%, transparent 58%, color-mix(in oklab, ${bright} 55%, 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.",
        "A full-bleed background — use mesh-gradient or aurora.",
        "More than one per view: the orb is the accent element; repeat it and it stops meaning anything."
      ]
    }
  }
}
