{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "thinking-orb",
  "title": "Thinking Orb",
  "description": "A breathing sphere whose motion says idle, listening, thinking or speaking without a word of text.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://miseui.junsu4621.workers.dev/r/motion.json"
  ],
  "files": [
    {
      "path": "registry/miseui/ui/thinking-orb.tsx",
      "content": "import type * as React from \"react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\nimport { SPRING_DEFAULT } from \"@/registry/miseui/lib/motion\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\ntype OrbState = \"idle\" | \"listening\" | \"thinking\" | \"speaking\"\n\nconst DEFAULT_SIZE_PX = 56\nconst FULL_TURN_DEGREES = 360\n\ninterface BodyLoop {\n  scale: number[]\n  seconds: number\n}\n\n/** Every loop starts and ends at 1, so switching state never lands more than one breath away. */\nconst BODY_LOOP: Record<OrbState, BodyLoop> = {\n  idle: { scale: [1, 1.03, 1], seconds: 4 },\n  listening: { scale: [1, 1.06, 1], seconds: 1.4 },\n  thinking: { scale: [1, 1.02, 1], seconds: 3 },\n  speaking: { scale: [1, 1.08, 1, 1.04, 1], seconds: 0.9 },\n}\n/** Idle sits back; every working state is fully lit. With reduced motion this tone is all that separates them. */\nconst IDLE_TONE = 0.7\nconst HALO_SECONDS = 1.4\nconst HALO_END_SCALE = 1.7\nconst HALO_START_OPACITY = 0.45\n/** Reduced motion: a still halo, a little outside the body. */\nconst HALO_STILL_SCALE = 1.25\nconst SWIRL_LAP_SECONDS = 6\nconst SWIRL_OPACITY = 0.7\n\n/**\n * Lit from the upper left, darkening to the rim: a sphere, not a disc. Light is mixed from white and\n * shade from black, not from theme tokens: those flip in dark mode and would light the sphere from below.\n */\nconst BODY_GRADIENT =\n  \"radial-gradient(circle at 35% 30%, color-mix(in oklch, var(--mise) 45%, white) 0%, var(--mise) 55%, color-mix(in oklch, var(--mise) 62%, black) 100%)\"\nconst SWIRL_LIGHT = \"radial-gradient(circle at 70% 68%, color-mix(in oklch, var(--mise) 20%, white) 0%, transparent 50%)\"\nconst SWIRL_SOFT = \"radial-gradient(circle at 28% 72%, color-mix(in oklch, var(--mise) 55%, white) 0%, transparent 46%)\"\n\ninterface ThinkingOrbProps extends React.ComponentProps<\"div\"> {\n  state?: OrbState\n  /** Diameter in px. */\n  size?: number\n  /** Accessible name. Falls back to the state. */\n  label?: string\n}\n\nfunction ThinkingOrb({\n  state = \"idle\",\n  size = DEFAULT_SIZE_PX,\n  label,\n  className,\n  style,\n  ...props\n}: ThinkingOrbProps) {\n  const reduceMotion = useReducedMotion()\n  const loop = BODY_LOOP[state]\n  const tone = state === \"idle\" ? IDLE_TONE : 1\n\n  return (\n    <div\n      role=\"img\"\n      aria-label={label ?? state}\n      data-slot=\"thinking-orb\"\n      data-state={state}\n      data-reduced={reduceMotion ? \"true\" : \"false\"}\n      className={cn(\"relative inline-block shrink-0\", className)}\n      style={{ ...style, width: size, height: size }}\n      {...props}\n    >\n      <AnimatePresence>\n        {state === \"listening\" ? (\n          <motion.span\n            key=\"halo\"\n            data-testid=\"orb-halo\"\n            className=\"absolute inset-0 rounded-full border border-mise\"\n            initial={{ opacity: 0, scale: 1 }}\n            animate={\n              reduceMotion\n                ? { opacity: HALO_START_OPACITY, scale: HALO_STILL_SCALE }\n                : { opacity: [HALO_START_OPACITY, 0], scale: [1, HALO_END_SCALE] }\n            }\n            exit={{ opacity: 0 }}\n            transition={reduceMotion ? SPRING_DEFAULT : { duration: HALO_SECONDS, ease: \"easeOut\", repeat: Infinity }}\n          />\n        ) : null}\n      </AnimatePresence>\n      <motion.span\n        data-testid=\"orb-body\"\n        className=\"absolute inset-0 rounded-full\"\n        style={{ backgroundImage: BODY_GRADIENT }}\n        animate={{ scale: reduceMotion ? 1 : loop.scale, opacity: tone }}\n        transition={{\n          scale: reduceMotion ? { duration: 0 } : { duration: loop.seconds, ease: \"easeInOut\", repeat: Infinity },\n          opacity: SPRING_DEFAULT,\n        }}\n      />\n      <span aria-hidden=\"true\" className=\"absolute inset-0 overflow-hidden rounded-full blur-[1.5px]\">\n        <Swirl gradient={SWIRL_LIGHT} direction={1} visible={state === \"thinking\"} still={Boolean(reduceMotion)} />\n        <Swirl gradient={SWIRL_SOFT} direction={-1} visible={state === \"thinking\"} still={Boolean(reduceMotion)} />\n      </span>\n    </div>\n  )\n}\n\ninterface SwirlProps {\n  gradient: string\n  direction: 1 | -1\n  visible: boolean\n  still: boolean\n}\n\n/**\n * Two off-centre pools of light turning against each other read as churn. They keep turning while\n * hidden so that showing them is a fade, never a jump to a new angle.\n */\nfunction Swirl({ gradient, direction, visible, still }: SwirlProps) {\n  return (\n    <motion.span\n      data-testid=\"orb-swirl\"\n      className=\"absolute inset-0 rounded-full\"\n      style={{ backgroundImage: gradient }}\n      initial={{ opacity: 0, rotate: 0 }}\n      animate={{ opacity: visible ? SWIRL_OPACITY : 0, rotate: still ? 0 : direction * FULL_TURN_DEGREES }}\n      transition={{\n        opacity: SPRING_DEFAULT,\n        rotate: still ? { duration: 0 } : { duration: SWIRL_LAP_SECONDS, ease: \"linear\", repeat: Infinity },\n      }}\n    />\n  )\n}\n\nexport { ThinkingOrb, type ThinkingOrbProps, type OrbState }\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "effects"
  ],
  "type": "registry:ui"
}