{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "voice-orb",
  "title": "Voice orb",
  "description": "A round orb for a voice assistant that turns faster while thinking and swells with the loudness of the voice.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/manniche/voice-orb/voice-orb.tsx",
      "content": "import { MotionConfig, motion, useSpring } from 'motion/react'\nimport { useEffect } from 'react'\nimport { cn } from '@/lib/utils'\n\nexport type VoiceOrbState = 'idle' | 'listening' | 'thinking' | 'speaking'\n\nexport type VoiceOrbProps = {\n  state: VoiceOrbState\n  /** 0 to 1, the loudness right now (from the microphone or the voice being played). */\n  level?: number\n  /** Diameter in px. */\n  size?: number\n  labels?: Partial<Record<VoiceOrbState, string>>\n  className?: string\n}\n\nconst EN: Record<VoiceOrbState, string> = { idle: 'Voice assistant ready', listening: 'Listening', thinking: 'Thinking', speaking: 'Speaking' }\n\n// How fast the colour layers turn in each state, in seconds per round.\nconst SPIN: Record<VoiceOrbState, [number, number]> = { idle: [18, 26], listening: [9, 13], thinking: [2.6, 3.8], speaking: [6, 8] }\n\n/**\n * A round orb for a voice assistant. Colour layers turn slowly when idle and fast while thinking,\n * and the orb swells with the loudness of whoever is talking.\n */\nexport function VoiceOrb({ state, level = 0, size = 160, labels = {}, className }: VoiceOrbProps) {\n  const t = { ...EN, ...labels }\n  const live = state === 'listening' || state === 'speaking'\n  const swell = useSpring(1, { stiffness: 300, damping: 30 })\n  const glow = useSpring(0.35, { stiffness: 200, damping: 30 })\n\n  useEffect(() => {\n    const l = live ? Math.min(1, Math.max(0, level)) : 0\n    swell.set(1 + l * 0.14)\n    glow.set(0.35 + l * 0.6)\n  }, [level, live, swell, glow])\n\n  const [a, b] = SPIN[state]\n\n  return (\n    <MotionConfig reducedMotion=\"user\">\n      <div role=\"status\" aria-label={t[state]} className={cn('relative inline-grid place-items-center', className)} style={{ width: size, height: size }}>\n        {/* Soft light around the orb, stronger when it's loud. */}\n        <motion.span aria-hidden className=\"absolute inset-0 rounded-full bg-[radial-gradient(closest-side,var(--color-primary),transparent)] blur-2xl\" style={{ opacity: glow, scale: swell }} />\n\n        <motion.span\n          aria-hidden\n          className=\"relative size-full overflow-hidden rounded-full bg-[oklch(0.25_0.06_265)] shadow-[inset_0_-8px_24px_rgb(0_0_0/0.35)]\"\n          style={{ scale: swell }}\n          animate={state === 'thinking' ? { opacity: [1, 0.8, 1] } : { opacity: 1 }}\n          transition={state === 'thinking' ? { duration: 1.4, repeat: Infinity, ease: 'easeInOut' } : { duration: 0.3 }}\n        >\n          {/* Two colour layers turning opposite ways; blurred so they melt into each other. */}\n          <span\n            className=\"absolute -inset-1/4 animate-spin bg-[conic-gradient(from_0deg,oklch(0.72_0.17_250),oklch(0.78_0.14_200),transparent_45%,oklch(0.7_0.2_300),transparent_80%)] blur-xl motion-reduce:animate-none\"\n            style={{ animationDuration: `${a}s` }}\n          />\n          <span\n            className=\"absolute -inset-1/4 animate-spin bg-[conic-gradient(from_120deg,transparent,oklch(0.85_0.12_180),transparent_35%,oklch(0.65_0.22_280),transparent_70%)] opacity-80 mix-blend-screen blur-lg motion-reduce:animate-none\"\n            style={{ animationDuration: `${b}s`, animationDirection: 'reverse' }}\n          />\n          {/* A glassy highlight on top. */}\n          <span className=\"absolute inset-0 rounded-full bg-[radial-gradient(circle_at_32%_26%,rgb(255_255_255/0.55),transparent_42%)]\" />\n        </motion.span>\n      </div>\n    </MotionConfig>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "categories": [
    "agent"
  ],
  "type": "registry:component"
}