{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ai-orb",
  "title": "AI Orb",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A glass orb of drifting theme-colored light that shows whether the assistant is idle, listening, thinking or speaking, and swells with a live voice level.",
  "dependencies": [
    "motion@^12",
    "class-variance-authority"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/ai-orb.tsx",
      "content": "// Ballmac UI: AI Orb. https://ui.ballmac.com/components/ai-orb\n\"use client\"\n\nimport * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { motion, useReducedMotion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst orbVariants = cva(\"relative isolate inline-block shrink-0 rounded-full\", {\n  variants: {\n    size: {\n      sm: \"size-8\",\n      default: \"size-16\",\n      lg: \"size-28\",\n      xl: \"size-44\",\n    },\n  },\n  defaultVariants: { size: \"default\" },\n})\n\ntype OrbState = \"idle\" | \"listening\" | \"thinking\" | \"speaking\"\n\ntype AiOrbProps = Omit<React.ComponentProps<\"div\">, \"children\"> &\n  VariantProps<typeof orbVariants> & {\n    /** What the assistant is doing. Each state changes how fast the colors drift and how the orb breathes. */\n    state?: OrbState\n    /** Live voice level from 0 to 1. While listening or speaking the orb swells with it. */\n    level?: number\n    /** Accessible name. Defaults to a sentence built from `state`. Pass `null` when text next to the orb already says it, which hides the orb from assistive tech. */\n    label?: string | null\n  }\n\nconst speeds: Record<OrbState, number> = { idle: 26, listening: 16, thinking: 6, speaking: 10 }\nconst breathing: Record<OrbState, { scale: number[]; duration: number }> = {\n  idle: { scale: [1, 1.03, 1], duration: 5 },\n  listening: { scale: [1, 1.05, 1], duration: 2.4 },\n  thinking: { scale: [1, 0.96, 1], duration: 1.6 },\n  speaking: { scale: [1, 1.07, 0.98, 1], duration: 1.3 },\n}\nconst stateNames: Record<OrbState, string> = {\n  idle: \"ready\",\n  listening: \"listening\",\n  thinking: \"thinking\",\n  speaking: \"speaking\",\n}\n\nfunction AiOrb({ state = \"idle\", level = 0, label, size, className, style, ...props }: AiOrbProps) {\n  const reduce = useReducedMotion()\n  const clamped = Math.min(1, Math.max(0, Number.isFinite(level) ? level : 0))\n  const live = state === \"listening\" || state === \"speaking\"\n  const swell = live ? 1 + clamped * 0.14 : 1\n  const name = label === undefined ? `Assistant is ${stateNames[state]}` : label\n  const spin = speeds[state]\n  const breath = breathing[state]\n\n  return (\n    <div\n      data-slot=\"ai-orb\"\n      data-state={state}\n      role={name === null ? undefined : \"img\"}\n      aria-label={name ?? undefined}\n      aria-hidden={name === null ? true : undefined}\n      className={cn(orbVariants({ size }), className)}\n      style={style}\n      {...props}\n    >\n      {/* Soft halo that follows the same swell. */}\n      <motion.span\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute -inset-[18%] -z-10 rounded-full bg-[radial-gradient(closest-side,color-mix(in_oklab,var(--chart-1)_45%,transparent),transparent)] opacity-70 blur-xl dark:opacity-60\"\n        animate={{ scale: reduce ? 1 : swell * 1.05 }}\n        transition={{ type: \"spring\", stiffness: 160, damping: 18 }}\n      />\n      <motion.div\n        aria-hidden=\"true\"\n        className=\"absolute inset-0 overflow-hidden rounded-full bg-background shadow-[inset_0_0_0_1px_rgb(255_255_255/0.22),inset_0_-8px_18px_-6px_rgb(0_0_0/0.25),0_6px_18px_-8px_rgb(0_0_0/0.35)]\"\n        animate={reduce ? { scale: 1 } : { scale: breath.scale.map((v) => v * swell) }}\n        transition={\n          reduce\n            ? undefined\n            : live && clamped > 0\n              ? { type: \"spring\", stiffness: 220, damping: 16 }\n              : { duration: breath.duration, repeat: Infinity, ease: \"easeInOut\" }\n        }\n      >\n        <motion.div\n          className=\"absolute -inset-[30%]\"\n          animate={reduce ? undefined : { rotate: 360 }}\n          transition={reduce ? undefined : { duration: spin, repeat: Infinity, ease: \"linear\" }}\n        >\n          <span className=\"absolute top-[12%] start-[10%] size-[62%] rounded-full bg-chart-1 opacity-90 blur-[14px]\" />\n          <span className=\"absolute end-[6%] bottom-[8%] size-[58%] rounded-full bg-chart-2 opacity-90 blur-[14px]\" />\n          <span className=\"absolute top-[38%] start-[34%] size-[48%] rounded-full bg-chart-4 opacity-80 blur-[12px]\" />\n        </motion.div>\n        <motion.div\n          className=\"absolute -inset-[30%]\"\n          animate={reduce ? undefined : { rotate: -360 }}\n          transition={reduce ? undefined : { duration: spin * 1.4, repeat: Infinity, ease: \"linear\" }}\n        >\n          <span className=\"absolute top-[6%] end-[18%] size-[40%] rounded-full bg-chart-5 opacity-70 blur-[12px]\" />\n          <span className=\"absolute bottom-[14%] start-[16%] size-[36%] rounded-full bg-chart-3 opacity-70 blur-[12px]\" />\n        </motion.div>\n        {/* Glass highlight. */}\n        <span className=\"absolute inset-0 rounded-full bg-[radial-gradient(120%_120%_at_30%_14%,rgb(255_255_255/0.55),transparent_46%)]\" />\n        <span className=\"absolute inset-[6%] rounded-full bg-[radial-gradient(closest-side,transparent_68%,rgb(255_255_255/0.16))]\" />\n      </motion.div>\n    </div>\n  )\n}\n\nexport { AiOrb, orbVariants, type AiOrbProps, type OrbState }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/ai-orb.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "ai",
      "orb",
      "voice",
      "assistant",
      "status",
      "visual"
    ],
    "version": "1.0.0",
    "updated": "2026-09-30",
    "ai": {
      "summary": "state is idle | listening | thinking | speaking. Pass level (0 to 1) to make it swell with audio. size is sm | default | lg | xl. Colors come from chart-1 to chart-5, so it follows the theme.",
      "whenToUse": [
        "The face of a voice assistant or a full-screen chat",
        "A status mark beside an assistant's name"
      ],
      "whenNotToUse": [
        "A plain spinner (use spinner)",
        "Anything that must be readable as text; pair it with thinking-indicator"
      ],
      "composesWith": [
        "voice-input",
        "thinking-indicator",
        "ai-chat"
      ],
      "a11y": [
        {
          "keys": "Screen readers",
          "action": "role=img named 'Assistant is listening' and so on. Pass label={null} to hide it when text beside it says the same"
        },
        {
          "keys": "Reduced motion",
          "action": "Static orb, no drift or breathing"
        }
      ],
      "customization": [
        "state",
        "level",
        "size",
        "label (string | null)"
      ]
    },
    "examples": [
      "ai-orb-demo",
      "ai-orb-voice"
    ],
    "url": "https://ui.ballmac.com/components/ai-orb"
  },
  "categories": [
    "ai"
  ],
  "type": "registry:ui"
}