{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "agent-orb",
  "title": "Agent Orb",
  "description": "A small living orb for an assistant's state: colour fields drift inside a glassy sphere and it breathes, idle, listening, thinking or speaking, with its speed tied to the token rate so it keeps pace with the stream.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://eased.dev/r/eased-tokens.json",
    "https://eased.dev/r/eased-motion.json"
  ],
  "files": [
    {
      "path": "src/registry/eased/agent-orb/agent-orb.tsx",
      "content": "\"use client\";\n\nimport { isMotionValue, motion, useInView, useMotionValue, useTransform, type MotionValue } from \"motion/react\";\nimport { useEffect, useRef, type ComponentType, type CSSProperties } from \"react\";\nimport { useSafeReducedMotion } from \"../lib/motion\";\n\nexport type OrbState = \"idle\" | \"listening\" | \"thinking\" | \"speaking\";\n\n/**\n * What every orb variant draws from. The orb owns the clock and the state; a variant only\n * paints. Pro variants (ring, swirl, pulse-dots, liquid, aurora, grain, shader) are\n * components of this shape passed as `variant`.\n */\nexport type OrbRenderProps = {\n  /** Diameter in px. */\n  size: number;\n  state: OrbState;\n  /**\n   * Animated time in seconds, already scaled by the orb's speed (idle 0.4×, thinking 1×,\n   * speaking up to 2× with the token rate). Derive every rotation and loop from it, so speed\n   * changes glide instead of jumping. Frozen under reduced motion and off-screen.\n   */\n  phase: MotionValue<number>;\n  /** 0–1, smoothed: how much is happening (state, token rate, voice level). Drive amplitude with it. */\n  energy: MotionValue<number>;\n  /** Reduced motion is on: draw a still frame. */\n  reduced: boolean;\n};\n\nexport type OrbVariant = ComponentType<OrbRenderProps>;\n\nexport type AgentOrbProps = {\n  /** Each state has its own colours, speed and energy, and crossfades into the next. Default `thinking`. */\n  state?: OrbState;\n  /**\n   * 0–1: how fast the stream is flowing. Speeds the swirl up to 2× and swells the breath.\n   * Pass `useTokenRate(text).rate` (a motion value) so the orb follows without re-rendering.\n   */\n  rate?: number | MotionValue<number>;\n  /** 0–1 input level while `listening` (a mic's amplitude); the orb swells with it. */\n  level?: number | MotionValue<number>;\n  /** `orb` (default), or a Pro variant component. */\n  variant?: \"orb\" | OrbVariant;\n  /** `sm` 16px (inline with text), `md` 24px, `lg` 40px, or any diameter in px. Default `md`. */\n  size?: \"sm\" | \"md\" | \"lg\" | number;\n  /** Accessible name. Default the state (\"Thinking\"); `false` when visible text already says it. */\n  label?: string | false;\n  className?: string;\n};\n\nconst SIZES = { sm: 16, md: 24, lg: 40 } as const;\nconst SPEED: Record<OrbState, number> = { idle: 0.4, listening: 0.7, thinking: 1, speaking: 1 };\nconst ENERGY: Record<OrbState, number> = { idle: 0.1, listening: 0.3, thinking: 0.45, speaking: 0.5 };\nconst NAMES: Record<OrbState, string> = { idle: \"Idle\", listening: \"Listening\", thinking: \"Thinking\", speaking: \"Speaking\" };\n\n/** Reads a number or a motion value without subscribing React to it. */\nfunction useReader(v: number | MotionValue<number> | undefined) {\n  const ref = useRef(v);\n  useEffect(() => {\n    ref.current = v;\n  });\n  return () => {\n    const x = ref.current;\n    return x === undefined ? 0 : isMotionValue(x) ? x.get() : x;\n  };\n}\n\n/**\n * The orb's clock: phase advances by speed × dt each frame, and speed and energy ease toward\n * their targets with a 0.5s time constant (critically damped, no overshoot), so a burst of\n * tokens swells the orb and a pause lets it settle. The loop runs only while on screen and\n * not under reduced motion.\n */\nfunction useOrbClock(state: OrbState, rate: () => number, level: () => number, active: boolean) {\n  const phase = useMotionValue(0);\n  const energy = useMotionValue(ENERGY[state]);\n  const speed = useRef(SPEED[state]);\n  const target = useRef({ state, rate, level });\n  useEffect(() => {\n    target.current = { state, rate, level };\n  });\n\n  useEffect(() => {\n    if (!active) return;\n    let raf = 0;\n    let then = performance.now();\n    const tick = (now: number) => {\n      const dt = Math.min(0.05, (now - then) / 1000);\n      then = now;\n      const { state, rate, level } = target.current;\n      const r = Math.min(1, Math.max(0, rate()));\n      const l = Math.min(1, Math.max(0, level()));\n      const k = 1 - Math.exp(-dt / 0.5);\n      const wantSpeed = SPEED[state] + (state === \"idle\" ? 0 : r);\n      const wantEnergy = Math.min(1, ENERGY[state] + (state === \"speaking\" ? r * 0.5 : state === \"thinking\" ? r * 0.3 : 0) + (state === \"listening\" ? l * 0.7 : 0));\n      speed.current += (wantSpeed - speed.current) * k;\n      energy.set(energy.get() + (wantEnergy - energy.get()) * k);\n      phase.set(phase.get() + dt * speed.current);\n      raf = requestAnimationFrame(tick);\n    };\n    raf = requestAnimationFrame(tick);\n    return () => cancelAnimationFrame(raf);\n  }, [active, phase, energy]);\n\n  return { phase, energy };\n}\n\n/*\n * Colours per state are registered properties, so a state change crossfades the gradients\n * over 700ms on its own. They are the orb's palette, declared here rather than in eased.css;\n * override `--eased-orb-a/b/c/deep` on the orb (per `data-state`) to re-theme it.\n */\nconst CSS = `\n@property --eased-orb-a { syntax: \"<color>\"; inherits: true; initial-value: transparent; }\n@property --eased-orb-b { syntax: \"<color>\"; inherits: true; initial-value: transparent; }\n@property --eased-orb-c { syntax: \"<color>\"; inherits: true; initial-value: transparent; }\n@property --eased-orb-deep { syntax: \"<color>\"; inherits: true; initial-value: transparent; }\n@property --eased-orb-sweep { syntax: \"<number>\"; inherits: true; initial-value: 0; }\n.eased-orb {\n  --eased-orb-a: oklch(66% 0.2 290); --eased-orb-b: oklch(72% 0.15 250); --eased-orb-c: oklch(81% 0.12 335); --eased-orb-deep: oklch(44% 0.17 282);\n  --eased-orb-sweep: 1;\n  transition: --eased-orb-a 700ms var(--eased-ease-standard), --eased-orb-b 700ms var(--eased-ease-standard), --eased-orb-c 700ms var(--eased-ease-standard), --eased-orb-deep 700ms var(--eased-ease-standard), --eased-orb-sweep 700ms var(--eased-ease-standard);\n}\n.eased-orb[data-state=\"idle\"] {\n  --eased-orb-a: oklch(76% 0.06 265); --eased-orb-b: oklch(84% 0.04 215); --eased-orb-c: oklch(74% 0.06 310); --eased-orb-deep: oklch(58% 0.06 270);\n  --eased-orb-sweep: 0;\n}\n.eased-orb[data-state=\"listening\"] {\n  --eased-orb-a: oklch(80% 0.12 205); --eased-orb-b: oklch(68% 0.15 245); --eased-orb-c: oklch(88% 0.1 175); --eased-orb-deep: oklch(48% 0.13 240);\n  --eased-orb-sweep: 0.35;\n}\n.eased-orb[data-state=\"speaking\"] {\n  --eased-orb-a: oklch(77% 0.15 55); --eased-orb-b: oklch(67% 0.19 22); --eased-orb-c: oklch(88% 0.11 88); --eased-orb-deep: oklch(52% 0.17 32);\n  --eased-orb-sweep: 0.25;\n}\n@media (forced-colors: active) { .eased-orb { forced-color-adjust: none; } }\n`;\n\n/**\n * A small living disc for an assistant's state: three soft colour fields drift around inside a\n * glassy sphere at different rates, a band of light sweeps through while thinking, and the\n * whole orb breathes. Its speed is a motion value: idle drifts at 0.4×, thinking turns at 1×,\n * and speaking runs up to 2× with the token `rate`, so it visibly keeps pace with the stream.\n * States crossfade their palettes. It pauses off-screen and stands still under reduced motion.\n */\nexport function AgentOrb({ state = \"thinking\", rate, level, variant = \"orb\", size = \"md\", label, className = \"\" }: AgentOrbProps) {\n  const ref = useRef<HTMLSpanElement>(null);\n  const inView = useInView(ref);\n  const reduced = useSafeReducedMotion();\n  const px = typeof size === \"number\" ? size : SIZES[size];\n  const { phase, energy } = useOrbClock(state, useReader(rate), useReader(level), inView && !reduced);\n  const Variant = variant === \"orb\" ? Orb : variant;\n  const name = label === false ? undefined : (label ?? NAMES[state]);\n\n  return (\n    <span\n      ref={ref}\n      role={name ? \"img\" : undefined}\n      aria-label={name}\n      aria-hidden={name ? undefined : true}\n      data-state={state}\n      className={`eased-orb relative inline-flex shrink-0 align-middle ${className}`}\n      style={{ width: px, height: px }}\n    >\n      <Variant size={px} state={state} phase={phase} energy={energy} reduced={reduced} />\n      <style href=\"eased-agent-orb\" precedence=\"default\">\n        {CSS}\n      </style>\n    </span>\n  );\n}\n\nconst layer = \"pointer-events-none absolute inset-[-25%] rounded-pill\";\n\n/**\n * The free variant: a slow iridescent swirl under three drifting colour fields and a dark\n * pocket that keeps the colour from going flat, a light sweep while thinking, and glass on top\n * (specular cap, a shaded lower edge, a fresnel rim). It breathes as one.\n */\nfunction Orb({ size, phase, energy, reduced }: OrbRenderProps) {\n  // Each layer orbits at its own rate (degrees per second at 1×), so the pattern never repeats.\n  const swirl = useTransform(phase, (p) => p * 28);\n  const r1 = useTransform(phase, (p) => p * 52);\n  const r2 = useTransform(phase, (p) => p * -67 + 120);\n  const r3 = useTransform(phase, (p) => p * 83 + 240);\n  const pocket = useTransform(phase, (p) => p * -38 + 60);\n  const sweep = useTransform(phase, (p) => p * 150);\n  // Breath: 1 → up to 1.06, faster as the orb speeds up, deeper with energy.\n  const scale = useTransform(() => {\n    const e = energy.get();\n    const wave = 0.5 + 0.5 * Math.sin((phase.get() * Math.PI * 2) / 2.6);\n    return 1 + (0.015 + 0.045 * e) * wave;\n  });\n  const halo = useTransform(energy, (e) => 0.35 + 0.65 * e);\n  const style = { \"--orb-blur\": `${Math.max(1, size * 0.07)}px` } as CSSProperties;\n\n  return (\n    <motion.span className=\"absolute inset-0\" style={{ scale: reduced ? 1 : scale }}>\n      {/* Glow behind the orb: ≤ 10% alpha (R36), brighter with energy. */}\n      <motion.span\n        aria-hidden\n        className=\"pointer-events-none absolute inset-[-35%] rounded-pill bg-[radial-gradient(closest-side,color-mix(in_oklch,var(--eased-orb-a)_10%,transparent),transparent)]\"\n        style={{ opacity: halo }}\n      />\n      <span\n        aria-hidden\n        style={style}\n        className=\"absolute inset-0 isolate overflow-hidden rounded-pill bg-[radial-gradient(circle_at_50%_40%,var(--eased-orb-b),var(--eased-orb-deep)_75%)]\"\n      >\n        <span className=\"absolute inset-0 [filter:blur(var(--orb-blur))_saturate(1.25)]\">\n          <motion.span className={`${layer} bg-[conic-gradient(from_0deg,var(--eased-orb-a),var(--eased-orb-b),var(--eased-orb-c),var(--eased-orb-deep),var(--eased-orb-a))] opacity-80`} style={{ rotate: swirl }} />\n          <motion.span className={`${layer} bg-[radial-gradient(circle_at_30%_32%,var(--eased-orb-a),transparent_30%)]`} style={{ rotate: r1 }} />\n          <motion.span className={`${layer} bg-[radial-gradient(circle_at_72%_62%,var(--eased-orb-b),transparent_32%)]`} style={{ rotate: r2 }} />\n          <motion.span className={`${layer} bg-[radial-gradient(circle_at_52%_80%,var(--eased-orb-c),transparent_26%)]`} style={{ rotate: r3 }} />\n          <motion.span className={`${layer} bg-[radial-gradient(circle_at_62%_38%,var(--eased-orb-deep),transparent_24%)] opacity-70`} style={{ rotate: pocket }} />\n          <motion.span\n            className={`${layer} bg-[conic-gradient(from_0deg,transparent_0_58%,oklch(100%_0_0/0.55)_78%,transparent_94%)] opacity-(--eased-orb-sweep) mix-blend-soft-light`}\n            style={{ rotate: sweep }}\n          />\n        </span>\n        {/* Glass: a specular cap, a shaded lower edge, a fresnel rim and a hairline. */}\n        <span className=\"absolute inset-0 rounded-pill bg-[radial-gradient(42%_30%_at_36%_20%,oklch(100%_0_0/0.62),transparent)]\" />\n        <span className=\"absolute inset-0 rounded-pill bg-[radial-gradient(80%_50%_at_60%_100%,oklch(0%_0_0/0.22),transparent)]\" />\n        <span className=\"absolute inset-0 rounded-pill bg-[radial-gradient(circle,transparent_58%,oklch(100%_0_0/0.22))] mix-blend-screen\" />\n        <span className=\"absolute inset-0 rounded-pill shadow-[inset_0_0_0_0.5px_oklch(100%_0_0/0.3)]\" />\n      </span>\n    </motion.span>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@components/eased/agent-orb/agent-orb.tsx"
    }
  ],
  "docs": "Import AgentOrb from components/eased/agent-orb/agent-orb. Feed rate from useTokenRate (eased-thinking-core) to tie its speed to your stream.",
  "type": "registry:ui"
}