"use client";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { EASE_OUT, SPRING_PANEL } from "@/lib/ease";
import { cn } from "@/lib/utils";
export type VoiceOrbState = "idle" | "listening" | "thinking" | "speaking";
export interface VoiceOrbProps {
/** Conversational state — drives breathing rhythm, hue drift, echo rings and the thinking sheen. */
state?: VoiceOrbState;
/** Orb diameter in pixels. */
size?: number;
className?: string;
}
// Brand blue (#339CFF — the library's accent, used elsewhere for focus rings)
// carried as an rgb triplet so the glow shadow can share it at low alpha.
const GLOW_RGB = "51, 156, 255";
const ORB_BACKGROUND =
"radial-gradient(circle at 30% 26%, #d9ecff 0%, #339CFF 38%, #1c5aa8 68%, #0a2540 100%)";
const ORB_SHADOW = [
`0 0 64px rgba(${GLOW_RGB}, 0.45)`,
`0 0 130px rgba(${GLOW_RGB}, 0.22)`,
"inset 0 -20px 42px rgba(4, 12, 26, 0.4)",
"inset 0 16px 28px rgba(255, 255, 255, 0.2)",
].join(", ");
/**
* Per-state breathing rhythm — the ball's own continuous `animate` target.
* These durations/eases are bespoke to the breathing/echo feel rather than UI
* transitions, so they live as named constants instead of `lib/ease.ts`
* tokens (which are tuned for <300ms UI moves and panel entrances): idle is
* a slow, low-lit drift; listening/speaking sync the scale with a hue or
* brightness pulse; thinking calms the breathing so the rotating sheen below
* carries the motion instead.
*/
const BREATH: Record<
VoiceOrbState,
{ scale: number[]; filter: string | string[]; duration: number }
> = {
idle: {
scale: [1, 1.03, 1],
filter: "brightness(0.85)",
duration: 5,
},
listening: {
scale: [1, 1.06, 1],
filter: [
"hue-rotate(0deg) brightness(1)",
"hue-rotate(15deg) brightness(1.05)",
"hue-rotate(0deg) brightness(1)",
],
duration: 2.4,
},
thinking: {
scale: [1, 1.02, 1],
filter: ["brightness(0.95)", "brightness(1.05)", "brightness(0.95)"],
duration: 3.2,
},
speaking: {
scale: [1, 1.09, 1.02, 1.12, 1],
filter: ["brightness(1)", "brightness(1.15)", "brightness(1)"],
duration: 1.6,
},
};
/** Expanding echo-ring, cloned twice with a stagger — a decelerating ripple radiating off the ball's edge. */
function EchoRing({ duration, delay }: { duration: number; delay: number }) {
return (