apex-ui-kit 2.2.0 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/ApexUI-Kit/templates/AiChat/AiChat.jsx +171 -0
  2. package/dist/ApexUI-Kit/templates/AiModelPrompt/AiModelPrompt.jsx +221 -0
  3. package/dist/ApexUI-Kit/templates/AiModelPrompt/index.js +2 -0
  4. package/dist/ApexUI-Kit/templates/AiModelPrompt/models.js +15 -0
  5. package/dist/ApexUI-Kit/templates/AiPromptBox/AiPromptBox.jsx +173 -0
  6. package/dist/ApexUI-Kit/templates/AiResponse/AiResponse.jsx +172 -0
  7. package/dist/ApexUI-Kit/templates/AiThinking/AiThinking.jsx +106 -0
  8. package/dist/ApexUI-Kit/templates/AiVoiceBars/AiVoiceBars.jsx +78 -0
  9. package/dist/ApexUI-Kit/templates/AiVoiceBlob/AiVoiceBlob.jsx +105 -0
  10. package/dist/ApexUI-Kit/templates/AiVoiceOrb/AiVoiceOrb.jsx +74 -0
  11. package/dist/ApexUI-Kit/templates/AiVoiceRing/AiVoiceRing.jsx +120 -0
  12. package/dist/ApexUI-Kit/templates/AiVoiceWave/AiVoiceWave.jsx +107 -0
  13. package/dist/ApexUI-Kit/templates/AnimatedSwitch/AnimatedSwitch.jsx +80 -0
  14. package/dist/ApexUI-Kit/templates/AnimatedTabs/AnimatedTabs.jsx +12 -4
  15. package/dist/ApexUI-Kit/templates/AuroraBackground/AuroraBackground.jsx +48 -0
  16. package/dist/ApexUI-Kit/templates/BounceCards/BounceCards.jsx +53 -0
  17. package/dist/ApexUI-Kit/templates/CardSwap3D/CardSwap3D.jsx +80 -0
  18. package/dist/ApexUI-Kit/templates/CircularText/CircularText.jsx +49 -0
  19. package/dist/ApexUI-Kit/templates/ClickSpark/ClickSpark.jsx +74 -0
  20. package/dist/ApexUI-Kit/templates/Coverflow3D/Coverflow3D.jsx +108 -0
  21. package/dist/ApexUI-Kit/templates/CubeGallery/CubeGallery.jsx +168 -0
  22. package/dist/ApexUI-Kit/templates/CursorTrail/CursorTrail.jsx +200 -0
  23. package/dist/ApexUI-Kit/templates/DotGlobe/DotGlobe.jsx +1 -0
  24. package/dist/ApexUI-Kit/templates/DynamicIsland/DynamicIsland.jsx +1 -1
  25. package/dist/ApexUI-Kit/templates/ElasticSlider/ElasticSlider.jsx +161 -0
  26. package/dist/ApexUI-Kit/templates/ExtrudedText/ExtrudedText.jsx +78 -0
  27. package/dist/ApexUI-Kit/templates/FaqAccordion/FaqAccordion.jsx +1 -1
  28. package/dist/ApexUI-Kit/templates/FlipBook/FlipBook.jsx +191 -0
  29. package/dist/ApexUI-Kit/templates/FloatingNavbar/FloatingNavbar.jsx +162 -0
  30. package/dist/ApexUI-Kit/templates/Folder/Folder.jsx +101 -0
  31. package/dist/ApexUI-Kit/templates/GlitchText/GlitchText.jsx +56 -0
  32. package/dist/ApexUI-Kit/templates/GooeyNav/GooeyNav.jsx +312 -0
  33. package/dist/ApexUI-Kit/templates/HoldToConfirm/HoldToConfirm.jsx +169 -0
  34. package/dist/ApexUI-Kit/templates/HoldToConfirm/UndoNotice.jsx +58 -0
  35. package/dist/ApexUI-Kit/templates/HoloCard/HoloCard.jsx +92 -0
  36. package/dist/ApexUI-Kit/templates/ImageCompare/ImageCompare.jsx +97 -0
  37. package/dist/ApexUI-Kit/templates/IsoStack/IsoStack.jsx +86 -0
  38. package/dist/ApexUI-Kit/templates/LoadingScreen/LoadingScreen.jsx +545 -101
  39. package/dist/ApexUI-Kit/templates/MagnetLines/MagnetLines.jsx +67 -0
  40. package/dist/ApexUI-Kit/templates/MagneticCursor/MagneticCursor.jsx +2 -160
  41. package/dist/ApexUI-Kit/templates/PixelTransition/PixelTransition.jsx +75 -0
  42. package/dist/ApexUI-Kit/templates/PointerFollower/PointerFollower.jsx +1 -1
  43. package/dist/ApexUI-Kit/templates/PricingCards/PricingCards.jsx +115 -0
  44. package/dist/ApexUI-Kit/templates/ProximityText/ProximityText.jsx +66 -0
  45. package/dist/ApexUI-Kit/templates/RotatingText/RotatingText.jsx +50 -0
  46. package/dist/ApexUI-Kit/templates/ShinyText/ShinyText.jsx +36 -0
  47. package/dist/ApexUI-Kit/templates/StarBorder/StarBorder.jsx +33 -0
  48. package/dist/ApexUI-Kit/templates/Stepper/Stepper.jsx +142 -0
  49. package/dist/ApexUI-Kit/templates/SwipeCards/SwipeCards.jsx +123 -0
  50. package/dist/ApexUI-Kit/templates/TextRing3D/TextRing3D.jsx +75 -0
  51. package/dist/ApexUI-Kit/templates/TrueFocus/TrueFocus.jsx +64 -0
  52. package/dist/ApexUI-Kit/templates/TypewriterText/TypewriterText.jsx +70 -0
  53. package/dist/cli/index.js +43 -0
  54. package/package.json +1 -1
  55. package/dist/ApexUI-Kit/templates/Carousel/DragCarousel.jsx +0 -105
@@ -0,0 +1,172 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { AnimatePresence, motion } from "framer-motion";
3
+ import { Bot, Check, Copy, Globe, RefreshCw, ThumbsDown, ThumbsUp } from "lucide-react";
4
+
5
+ /*
6
+ * AI Response: an assistant answer that streams in, with paragraphs, a code
7
+ * block (with its own copy button), source chips and an action bar — copy,
8
+ * regenerate, thumbs up / down. Blocks are plain objects, so it is easy to
9
+ * feed from your own model.
10
+ */
11
+
12
+ const DEFAULT_BLOCKS = [
13
+ { type: "text", text: "To make a card lift smoothly on hover, animate only transform and box-shadow. Here's a tiny Tailwind version:" },
14
+ { type: "code", lang: "jsx", text: '<div className="transition duration-300 hover:-translate-y-1 hover:shadow-xl">\n Card\n</div>' },
15
+ { type: "text", text: "Transforms run on the GPU, so the motion stays smooth even on low-end phones." },
16
+ ];
17
+ const DEFAULT_SOURCES = [
18
+ { title: "MDN: CSS transitions", href: "https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_transitions" },
19
+ { title: "web.dev: Animations", href: "https://web.dev/articles/animations-guide" },
20
+ ];
21
+
22
+ function CodeBlock({ lang, text, dark }) {
23
+ const [copied, setCopied] = useState(false);
24
+ return (
25
+ <div className={`overflow-hidden rounded-xl border ${dark ? "border-white/10 bg-black/40" : "border-black/10 bg-neutral-50"}`}>
26
+ <div className={`flex items-center justify-between px-3 py-1.5 text-[0.7rem] ${dark ? "bg-white/[0.04] text-white/50" : "bg-black/[0.03] text-neutral-500"}`}>
27
+ <span className="font-mono">{lang}</span>
28
+ <button
29
+ type="button"
30
+ onClick={async () => {
31
+ try {
32
+ await navigator.clipboard.writeText(text);
33
+ setCopied(true);
34
+ setTimeout(() => setCopied(false), 1400);
35
+ } catch {
36
+ /* clipboard blocked */
37
+ }
38
+ }}
39
+ className="inline-flex items-center gap-1 hover:opacity-100"
40
+ >
41
+ {copied ? <Check className="h-3 w-3" /> : <Copy className="h-3 w-3" />} {copied ? "Copied" : "Copy"}
42
+ </button>
43
+ </div>
44
+ <pre className="overflow-x-auto p-3 font-mono text-[0.78rem] leading-relaxed">{text}</pre>
45
+ </div>
46
+ );
47
+ }
48
+
49
+ const AiResponse = ({ blocks = DEFAULT_BLOCKS, sources = DEFAULT_SOURCES, speed = 18, assistant = "Apex AI", accentColor = "#a3e635", theme = "dark", onRegenerate, onFeedback, className = "" }) => {
50
+ const [run, setRun] = useState(0);
51
+ const [chars, setChars] = useState(0);
52
+ const [vote, setVote] = useState(null);
53
+ const [copied, setCopied] = useState(false);
54
+ const total = blocks.reduce((n, b) => n + b.text.length, 0);
55
+ const done = chars >= total;
56
+ const dark = theme !== "light";
57
+
58
+ useEffect(() => {
59
+ setChars(0);
60
+ const id = setInterval(() => setChars((c) => (c >= total ? (clearInterval(id), c) : c + 3)), speed);
61
+ return () => clearInterval(id);
62
+ }, [run, total, speed]);
63
+
64
+ // Cut the blocks at the current character count.
65
+ let left = chars;
66
+ const visible = [];
67
+ for (const b of blocks) {
68
+ if (left <= 0) break;
69
+ visible.push({ ...b, text: b.text.slice(0, left), partial: left < b.text.length });
70
+ left -= b.text.length;
71
+ }
72
+
73
+ const copyAll = async () => {
74
+ try {
75
+ await navigator.clipboard.writeText(blocks.map((b) => b.text).join("\n\n"));
76
+ setCopied(true);
77
+ setTimeout(() => setCopied(false), 1400);
78
+ } catch {
79
+ /* clipboard blocked */
80
+ }
81
+ };
82
+
83
+ const muted = dark ? "text-white/55" : "text-neutral-500";
84
+ const iconBtn = `grid h-8 w-8 place-items-center rounded-lg transition ${dark ? "hover:bg-white/10" : "hover:bg-black/5"}`;
85
+
86
+ return (
87
+ <div className={`w-full max-w-xl rounded-3xl border p-5 ${dark ? "border-white/10 bg-neutral-950 text-white" : "border-black/10 bg-white text-neutral-900"} ${className}`}>
88
+ <div className="mb-3 flex items-center gap-2.5">
89
+ <span className="grid h-8 w-8 place-items-center rounded-xl text-black" style={{ background: accentColor }}>
90
+ <Bot className="h-4 w-4" />
91
+ </span>
92
+ <span className="text-[0.88rem] font-semibold">{assistant}</span>
93
+ {!done && <span className={`text-[0.72rem] ${muted}`}>writing…</span>}
94
+ </div>
95
+
96
+ <div className="space-y-3 text-[0.92rem] leading-relaxed" aria-live="polite">
97
+ {visible.map((b, i) =>
98
+ b.type === "code" ? (
99
+ <CodeBlock key={i} lang={b.lang} text={b.text} dark={dark} />
100
+ ) : (
101
+ <p key={i} className={dark ? "text-white/85" : "text-neutral-700"}>
102
+ {b.text}
103
+ {b.partial && <span className="ml-0.5 inline-block h-4 w-1.5 animate-pulse rounded-sm align-middle" style={{ background: accentColor }} />}
104
+ </p>
105
+ )
106
+ )}
107
+ </div>
108
+
109
+ <AnimatePresence>
110
+ {done && (
111
+ <motion.div initial={{ opacity: 0, y: 6 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0 }}>
112
+ {sources.length > 0 && (
113
+ <div className="mt-4 flex flex-wrap gap-2">
114
+ {sources.map((s, i) => (
115
+ <a key={s.href} href={s.href} target="_blank" rel="noreferrer" className={`inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-[0.72rem] transition ${dark ? "border-white/10 hover:border-white/25" : "border-black/10 hover:border-black/20"}`}>
116
+ <span className="grid h-4 w-4 place-items-center rounded-full text-[0.6rem] font-bold text-black" style={{ background: accentColor }}>
117
+ {i + 1}
118
+ </span>
119
+ <Globe className="h-3 w-3 opacity-60" />
120
+ {s.title}
121
+ </a>
122
+ ))}
123
+ </div>
124
+ )}
125
+ <div className={`mt-4 flex items-center gap-1 border-t pt-3 ${dark ? "border-white/10" : "border-black/10"}`}>
126
+ <button type="button" aria-label="Copy answer" onClick={copyAll} className={iconBtn}>
127
+ {copied ? <Check className="h-4 w-4" style={{ color: accentColor }} /> : <Copy className="h-4 w-4" />}
128
+ </button>
129
+ <button
130
+ type="button"
131
+ aria-label="Regenerate"
132
+ onClick={() => {
133
+ setVote(null);
134
+ setRun((r) => r + 1);
135
+ onRegenerate?.();
136
+ }}
137
+ className={iconBtn}
138
+ >
139
+ <RefreshCw className="h-4 w-4" />
140
+ </button>
141
+ <span className={`mx-1 h-4 w-px ${dark ? "bg-white/10" : "bg-black/10"}`} />
142
+ {[
143
+ ["up", ThumbsUp, "Good answer"],
144
+ ["down", ThumbsDown, "Bad answer"],
145
+ ].map(([key, Icon, label]) => (
146
+ <motion.button
147
+ key={key}
148
+ type="button"
149
+ aria-label={label}
150
+ aria-pressed={vote === key}
151
+ whileTap={{ scale: 0.8 }}
152
+ onClick={() => {
153
+ const v = vote === key ? null : key;
154
+ setVote(v);
155
+ onFeedback?.(v);
156
+ }}
157
+ className={iconBtn}
158
+ style={vote === key ? { color: accentColor } : undefined}
159
+ >
160
+ <Icon className="h-4 w-4" fill={vote === key ? "currentColor" : "none"} />
161
+ </motion.button>
162
+ ))}
163
+ <span className={`ml-auto text-[0.7rem] ${muted}`}>{vote === "up" ? "Thanks for the feedback!" : vote === "down" ? "We'll do better." : ""}</span>
164
+ </div>
165
+ </motion.div>
166
+ )}
167
+ </AnimatePresence>
168
+ </div>
169
+ );
170
+ };
171
+
172
+ export default AiResponse;
@@ -0,0 +1,106 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { AnimatePresence, motion } from "framer-motion";
3
+ import { Brain, Check, ChevronDown, Loader2 } from "lucide-react";
4
+
5
+ /*
6
+ * AI Thinking: shows an assistant working through its steps. A shimmering
7
+ * "Thinking" label with a live timer, steps that tick off one by one, and a
8
+ * collapsible panel for the reasoning. Drive `step` yourself, or let it play.
9
+ */
10
+
11
+ const DEFAULT_STEPS = [
12
+ { label: "Understanding the question", detail: "Looking for the goal and the constraints." },
13
+ { label: "Searching 12 sources", detail: "Docs, changelogs and two blog posts look relevant." },
14
+ { label: "Comparing approaches", detail: "CSS transforms win: no layout, GPU friendly." },
15
+ { label: "Writing the answer", detail: "Short summary first, then the code." },
16
+ ];
17
+
18
+ const AiThinking = ({ steps = DEFAULT_STEPS, step: stepProp, stepDuration = 1400, loop = true, accentColor = "#a3e635", theme = "dark", defaultOpen = true, className = "" }) => {
19
+ const [auto, setAuto] = useState(0);
20
+ const [seconds, setSeconds] = useState(0);
21
+ const [open, setOpen] = useState(defaultOpen);
22
+ const controlled = typeof stepProp === "number";
23
+ const current = controlled ? stepProp : auto;
24
+ const done = current >= steps.length;
25
+ const dark = theme !== "light";
26
+
27
+ useEffect(() => {
28
+ if (controlled) return undefined;
29
+ const id = setTimeout(() => setAuto((s) => (s >= steps.length ? (loop ? 0 : s) : s + 1)), current >= steps.length ? 2400 : stepDuration);
30
+ return () => clearTimeout(id);
31
+ }, [controlled, current, steps.length, stepDuration, loop]);
32
+
33
+ useEffect(() => {
34
+ if (current === 0) setSeconds(0);
35
+ if (done) return undefined;
36
+ const id = setInterval(() => setSeconds((s) => s + 0.1), 100);
37
+ return () => clearInterval(id);
38
+ }, [done, current]);
39
+
40
+ const muted = dark ? "text-white/50" : "text-neutral-500";
41
+
42
+ return (
43
+ <div className={`w-full max-w-md rounded-3xl border p-4 ${dark ? "border-white/10 bg-neutral-950 text-white" : "border-black/10 bg-white text-neutral-900"} ${className}`}>
44
+ <style>{"@keyframes apex-shimmer{from{background-position:200% 0}to{background-position:-200% 0}}"}</style>
45
+ <button type="button" onClick={() => setOpen((o) => !o)} aria-expanded={open} className="flex w-full items-center gap-3 text-left">
46
+ <span className="relative grid h-9 w-9 place-items-center rounded-xl" style={{ background: `color-mix(in srgb, ${accentColor} 18%, transparent)`, color: accentColor }}>
47
+ {!done && <motion.span className="absolute inset-0 rounded-xl border-2" style={{ borderColor: accentColor }} animate={{ opacity: [0.6, 0, 0.6], scale: [1, 1.25, 1] }} transition={{ duration: 1.6, repeat: Infinity }} />}
48
+ {done ? <Check className="h-5 w-5" /> : <Brain className="h-5 w-5" />}
49
+ </span>
50
+ <span className="flex-1">
51
+ <span
52
+ className="block text-[0.95rem] font-semibold"
53
+ style={
54
+ done
55
+ ? undefined
56
+ : {
57
+ backgroundImage: `linear-gradient(90deg, ${dark ? "#fff6" : "#0006"} 0%, ${dark ? "#fff" : "#000"} 50%, ${dark ? "#fff6" : "#0006"} 100%)`,
58
+ backgroundSize: "200% 100%",
59
+ WebkitBackgroundClip: "text",
60
+ backgroundClip: "text",
61
+ color: "transparent",
62
+ animation: "apex-shimmer 1.6s linear infinite",
63
+ }
64
+ }
65
+ >
66
+ {done ? "Thought it through" : "Thinking"}
67
+ </span>
68
+ <span className={`text-[0.75rem] tabular-nums ${muted}`}>{done ? `Done in ${seconds.toFixed(1)}s` : `${seconds.toFixed(1)}s · step ${Math.min(current + 1, steps.length)} of ${steps.length}`}</span>
69
+ </span>
70
+ <ChevronDown className={`h-4 w-4 transition-transform ${open ? "rotate-180" : ""} ${muted}`} />
71
+ </button>
72
+
73
+ <AnimatePresence initial={false}>
74
+ {open && (
75
+ <motion.ol initial={{ height: 0, opacity: 0 }} animate={{ height: "auto", opacity: 1 }} exit={{ height: 0, opacity: 0 }} transition={{ duration: 0.28, ease: [0.22, 1, 0.36, 1] }} className="relative mt-4 overflow-hidden pl-3">
76
+ {/* Through the circle centres: list padding (0.75rem) + half a circle (0.575rem). */}
77
+ <span aria-hidden="true" className={`absolute bottom-3 left-[calc(1.325rem-0.5px)] top-3 w-px ${dark ? "bg-white/10" : "bg-black/10"}`} />
78
+ {steps.map((s, i) => {
79
+ const state = i < current ? "done" : i === current && !done ? "active" : "todo";
80
+ return (
81
+ <motion.li key={s.label} initial={{ opacity: 0, x: -6 }} animate={{ opacity: state === "todo" ? 0.4 : 1, x: 0 }} transition={{ delay: i * 0.05 }} className="relative flex gap-3 pb-3.5 last:pb-0">
82
+ <span className="relative z-10 mt-0.5 grid h-[1.15rem] w-[1.15rem] shrink-0 place-items-center rounded-full" style={{ background: state === "done" ? accentColor : dark ? "#262626" : "#e5e5e5", color: "#0a0a0a" }}>
83
+ {state === "done" && <Check className="h-3 w-3" strokeWidth={3} />}
84
+ {state === "active" && <Loader2 className="h-3 w-3 animate-spin" style={{ color: accentColor }} />}
85
+ </span>
86
+ <span>
87
+ <span className="block text-[0.85rem] font-medium">{s.label}</span>
88
+ <AnimatePresence>
89
+ {state !== "todo" && s.detail && (
90
+ <motion.span initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: "auto" }} className={`block text-[0.78rem] ${muted}`}>
91
+ {s.detail}
92
+ </motion.span>
93
+ )}
94
+ </AnimatePresence>
95
+ </span>
96
+ </motion.li>
97
+ );
98
+ })}
99
+ </motion.ol>
100
+ )}
101
+ </AnimatePresence>
102
+ </div>
103
+ );
104
+ };
105
+
106
+ export default AiThinking;
@@ -0,0 +1,78 @@
1
+ import React, { useEffect, useRef, useState } from "react";
2
+ import { AnimatePresence, motion } from "framer-motion";
3
+ import { Mic, MicOff } from "lucide-react";
4
+
5
+ /*
6
+ * AI Voice Bars: a pill of rounded equalizer bars, mirrored from the middle.
7
+ * Short dots when idle, lively while listening, a travelling ripple while
8
+ * thinking and tall bursts while speaking. Tap to cycle, or set `state`.
9
+ */
10
+
11
+ const LABELS = { idle: "Tap to talk", listening: "Listening…", thinking: "Thinking…", speaking: "Speaking…" };
12
+ const ORDER = ["idle", "listening", "thinking", "speaking"];
13
+
14
+ const AiVoiceBars = ({ state: stateProp, onStateChange, count = 7, color = "#a3e635", theme = "dark", showLabel = true, className = "" }) => {
15
+ const [inner, setInner] = useState("idle");
16
+ const state = stateProp ?? inner;
17
+ const stateRef = useRef(state);
18
+ stateRef.current = state;
19
+ const bars = useRef([]);
20
+ const dark = theme !== "light";
21
+
22
+ // Bar heights are written straight to the DOM each frame (no re-renders).
23
+ useEffect(() => {
24
+ let raf = 0;
25
+ const heights = Array(count).fill(0.15);
26
+ const tick = (now) => {
27
+ const t = now / 1000;
28
+ const s = stateRef.current;
29
+ for (let i = 0; i < count; i++) {
30
+ const centre = 1 - Math.abs(i - (count - 1) / 2) / ((count - 1) / 2 + 0.5);
31
+ let target = 0.14;
32
+ if (s === "listening") target = 0.25 + 0.6 * centre * Math.abs(Math.sin(t * 6 + i * 1.3) * Math.cos(t * 2.2 + i));
33
+ if (s === "thinking") target = 0.2 + 0.35 * Math.max(0, Math.sin(t * 4 - i * 0.9));
34
+ if (s === "speaking") target = 0.3 + 0.7 * centre * Math.abs(Math.sin(t * 9 + i * 2.1) * Math.sin(t * 2.7 + i * 0.6));
35
+ heights[i] += (target - heights[i]) * 0.25;
36
+ const el = bars.current[i];
37
+ if (el) el.style.transform = `scaleY(${Math.max(0.12, heights[i])})`;
38
+ }
39
+ raf = requestAnimationFrame(tick);
40
+ };
41
+ raf = requestAnimationFrame(tick);
42
+ return () => cancelAnimationFrame(raf);
43
+ }, [count]);
44
+
45
+ const next = () => {
46
+ const n = ORDER[(ORDER.indexOf(state) + 1) % ORDER.length];
47
+ if (stateProp === undefined) setInner(n);
48
+ onStateChange?.(n);
49
+ };
50
+
51
+ return (
52
+ <div className={`flex flex-col items-center gap-5 ${className}`}>
53
+ <button
54
+ type="button"
55
+ onClick={next}
56
+ aria-label={`Voice assistant: ${LABELS[state]}`}
57
+ className={`flex h-28 items-center gap-2.5 rounded-full px-8 outline-none transition-shadow duration-500 focus-visible:ring-2 focus-visible:ring-white/30 ${dark ? "bg-neutral-950 ring-1 ring-white/10" : "bg-white ring-1 ring-black/10"}`}
58
+ style={{ boxShadow: state === "idle" ? "none" : `0 0 50px -12px ${color}` }}
59
+ >
60
+ {Array.from({ length: count }, (_, i) => (
61
+ <span key={i} ref={(el) => (bars.current[i] = el)} className="block h-16 w-3 rounded-full will-change-transform" style={{ background: color, transform: "scaleY(0.15)" }} />
62
+ ))}
63
+ </button>
64
+ {showLabel && (
65
+ <div className="flex items-center gap-2 text-sm font-medium">
66
+ {state === "idle" ? <MicOff className="h-4 w-4 opacity-60" /> : <Mic className="h-4 w-4" style={{ color }} />}
67
+ <AnimatePresence mode="wait">
68
+ <motion.span key={state} initial={{ opacity: 0, y: 6 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -6 }} transition={{ duration: 0.18 }}>
69
+ {LABELS[state]}
70
+ </motion.span>
71
+ </AnimatePresence>
72
+ </div>
73
+ )}
74
+ </div>
75
+ );
76
+ };
77
+
78
+ export default AiVoiceBars;
@@ -0,0 +1,105 @@
1
+ import React, { useEffect, useId, useRef, useState } from "react";
2
+ import { AnimatePresence, motion } from "framer-motion";
3
+ import { Mic, MicOff } from "lucide-react";
4
+
5
+ /*
6
+ * AI Voice Blob: a soft gradient blob whose outline wobbles with the voice.
7
+ * Nearly round when idle, rippling while listening, slowly morphing while
8
+ * thinking and bouncing while speaking. Drawn as one smooth SVG path that
9
+ * updates each frame. Tap to cycle, or set `state`.
10
+ */
11
+
12
+ const LABELS = { idle: "Tap to talk", listening: "Listening…", thinking: "Thinking…", speaking: "Speaking…" };
13
+ const ORDER = ["idle", "listening", "thinking", "speaking"];
14
+ const POINTS = 10;
15
+
16
+ // Closed smooth curve through the points (Catmull-Rom → Bézier).
17
+ const smoothPath = (pts) => {
18
+ const n = pts.length;
19
+ let d = `M${pts[0][0]},${pts[0][1]}`;
20
+ for (let i = 0; i < n; i++) {
21
+ const p0 = pts[(i - 1 + n) % n];
22
+ const p1 = pts[i];
23
+ const p2 = pts[(i + 1) % n];
24
+ const p3 = pts[(i + 2) % n];
25
+ d += ` C${p1[0] + (p2[0] - p0[0]) / 6},${p1[1] + (p2[1] - p0[1]) / 6} ${p2[0] - (p3[0] - p1[0]) / 6},${p2[1] - (p3[1] - p1[1]) / 6} ${p2[0]},${p2[1]}`;
26
+ }
27
+ return `${d}Z`;
28
+ };
29
+
30
+ const AiVoiceBlob = ({ state: stateProp, onStateChange, colors = ["#a3e635", "#22d3ee", "#a78bfa"], size = 13, showLabel = true, className = "" }) => {
31
+ const gid = useId().replace(/[^a-zA-Z0-9]/g, "");
32
+ const pathRef = useRef(null);
33
+ const glowRef = useRef(null);
34
+ const [inner, setInner] = useState("idle");
35
+ const state = stateProp ?? inner;
36
+ const stateRef = useRef(state);
37
+ stateRef.current = state;
38
+
39
+ useEffect(() => {
40
+ let raf = 0;
41
+ let energy = 0.05;
42
+ const tick = (now) => {
43
+ const t = now / 1000;
44
+ const s = stateRef.current;
45
+ const target = s === "speaking" ? 0.16 + 0.12 * Math.abs(Math.sin(t * 7)) : s === "listening" ? 0.12 + 0.06 * Math.abs(Math.sin(t * 4.3)) : s === "thinking" ? 0.1 : 0.035;
46
+ energy += (target - energy) * 0.08;
47
+ const speed = s === "thinking" ? 0.8 : s === "idle" ? 0.6 : 2.4;
48
+ const pts = [];
49
+ for (let i = 0; i < POINTS; i++) {
50
+ const a = (i / POINTS) * Math.PI * 2;
51
+ const wob = Math.sin(t * speed + i * 1.7) * 0.6 + Math.sin(t * speed * 1.7 + i * 2.9) * 0.4;
52
+ const r = 40 * (1 + energy * wob);
53
+ pts.push([50 + Math.cos(a) * r, 50 + Math.sin(a) * r]);
54
+ }
55
+ const d = smoothPath(pts);
56
+ pathRef.current?.setAttribute("d", d);
57
+ glowRef.current?.setAttribute("d", d);
58
+ raf = requestAnimationFrame(tick);
59
+ };
60
+ raf = requestAnimationFrame(tick);
61
+ return () => cancelAnimationFrame(raf);
62
+ }, []);
63
+
64
+ const next = () => {
65
+ const n = ORDER[(ORDER.indexOf(state) + 1) % ORDER.length];
66
+ if (stateProp === undefined) setInner(n);
67
+ onStateChange?.(n);
68
+ };
69
+
70
+ return (
71
+ <div className={`flex flex-col items-center gap-4 ${className}`}>
72
+ <button type="button" onClick={next} aria-label={`Voice assistant: ${LABELS[state]}`} className="rounded-full outline-none focus-visible:ring-2 focus-visible:ring-white/30" style={{ width: `min(${size}rem, 76vw)`, aspectRatio: "1" }}>
73
+ <svg viewBox="0 0 100 100" className="h-full w-full overflow-visible" aria-hidden="true">
74
+ <defs>
75
+ <linearGradient id={`g${gid}`} x1="0" y1="0" x2="1" y2="1">
76
+ {colors.map((c, i) => (
77
+ <stop key={c} offset={`${(i / Math.max(1, colors.length - 1)) * 100}%`} stopColor={c} />
78
+ ))}
79
+ </linearGradient>
80
+ <radialGradient id={`h${gid}`} cx="0.35" cy="0.3" r="0.6">
81
+ <stop offset="0%" stopColor="#fff" stopOpacity="0.55" />
82
+ <stop offset="100%" stopColor="#fff" stopOpacity="0" />
83
+ </radialGradient>
84
+ </defs>
85
+ {/* A wider, faint copy works as the glow without a blur filter. */}
86
+ <path ref={glowRef} fill={`url(#g${gid})`} opacity="0.25" transform="translate(50 50) scale(1.14) translate(-50 -50)" />
87
+ <path ref={pathRef} fill={`url(#g${gid})`} />
88
+ <circle cx="40" cy="38" r="22" fill={`url(#h${gid})`} />
89
+ </svg>
90
+ </button>
91
+ {showLabel && (
92
+ <div className="flex items-center gap-2 text-sm font-medium">
93
+ {state === "idle" ? <MicOff className="h-4 w-4 opacity-60" /> : <Mic className="h-4 w-4" style={{ color: colors[0] }} />}
94
+ <AnimatePresence mode="wait">
95
+ <motion.span key={state} initial={{ opacity: 0, y: 6 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -6 }} transition={{ duration: 0.18 }}>
96
+ {LABELS[state]}
97
+ </motion.span>
98
+ </AnimatePresence>
99
+ </div>
100
+ )}
101
+ </div>
102
+ );
103
+ };
104
+
105
+ export default AiVoiceBlob;
@@ -0,0 +1,74 @@
1
+ import React, { useState } from "react";
2
+ import { AnimatePresence, motion } from "framer-motion";
3
+ import { Mic, MicOff } from "lucide-react";
4
+
5
+ /*
6
+ * AI Voice Orb: the "face" of a voice assistant. Soft colour blobs swirl
7
+ * inside a glass sphere and the whole orb reacts to its state — calm when
8
+ * idle, pulsing while listening, spinning while thinking and rippling while
9
+ * speaking. Tap it (or control `state`) to switch between them.
10
+ */
11
+
12
+ const STATES = {
13
+ idle: { label: "Tap to talk", scale: [1, 1.02, 1], spin: 14, speed: 3.2 },
14
+ listening: { label: "Listening…", scale: [1, 1.1, 0.97, 1.08, 1], spin: 8, speed: 1.1 },
15
+ thinking: { label: "Thinking…", scale: [0.96, 1, 0.96], spin: 2.2, speed: 1.4 },
16
+ speaking: { label: "Speaking…", scale: [1, 1.06, 1, 1.12, 1, 1.04, 1], spin: 5, speed: 1.3 },
17
+ };
18
+ const ORDER = ["idle", "listening", "thinking", "speaking"];
19
+
20
+ const AiVoiceOrb = ({ state: stateProp, onStateChange, colors = ["#a3e635", "#22d3ee", "#a78bfa", "#f472b6"], size = 12, showLabel = true, className = "" }) => {
21
+ const [inner, setInner] = useState("idle");
22
+ const state = stateProp ?? inner;
23
+ const s = STATES[state] ?? STATES.idle;
24
+
25
+ const next = () => {
26
+ const n = ORDER[(ORDER.indexOf(state) + 1) % ORDER.length];
27
+ if (stateProp === undefined) setInner(n);
28
+ onStateChange?.(n);
29
+ };
30
+
31
+ return (
32
+ <div className={`flex flex-col items-center gap-6 ${className}`}>
33
+ <button type="button" onClick={next} aria-label={`Voice assistant: ${s.label}`} className="relative grid place-items-center rounded-full outline-none focus-visible:ring-4 focus-visible:ring-white/30" style={{ width: `${size}rem`, height: `${size}rem` }}>
34
+ {/* Ripples while speaking or listening */}
35
+ {(state === "speaking" || state === "listening") &&
36
+ [0, 1, 2].map((i) => (
37
+ <motion.span key={`${state}-${i}`} className="absolute inset-0 rounded-full border" style={{ borderColor: colors[i % colors.length] }} initial={{ scale: 1, opacity: 0.5 }} animate={{ scale: 1.6, opacity: 0 }} transition={{ duration: 2, repeat: Infinity, delay: i * 0.6, ease: "easeOut" }} />
38
+ ))}
39
+ {/* Outer glow: a soft gradient that only pulses (no blur filter, so it stays cheap). */}
40
+ <motion.span className="absolute inset-[-22%] rounded-full" style={{ background: `radial-gradient(circle, ${colors[0]}55 0%, ${colors[2 % colors.length]}33 35%, transparent 68%)` }} animate={{ opacity: [0.6, 1, 0.6], scale: [0.96, 1.04, 0.96] }} transition={{ duration: s.speed * 1.4, repeat: Infinity, ease: "easeInOut" }} />
41
+ {/* The sphere */}
42
+ <motion.span className="relative block h-full w-full overflow-hidden rounded-full bg-neutral-950 shadow-[inset_0_0_40px_rgba(255,255,255,0.08)]" animate={{ scale: s.scale }} transition={{ duration: s.speed, repeat: Infinity, ease: "easeInOut" }}>
43
+ <motion.span className="absolute inset-0 will-change-transform" animate={{ rotate: 360 }} transition={{ duration: s.spin, repeat: Infinity, ease: "linear" }}>
44
+ {colors.map((c, i) => (
45
+ <motion.span
46
+ key={c}
47
+ className="absolute h-3/4 w-3/4 rounded-full will-change-transform"
48
+ // Soft-edged blobs from gradients instead of a blur filter.
49
+ style={{ background: `radial-gradient(circle, ${c} 0%, ${c}aa 30%, transparent 68%)`, mixBlendMode: "screen", left: `${[0, 30, 10, 35][i % 4]}%`, top: `${[5, 0, 35, 30][i % 4]}%` }}
50
+ animate={{ x: ["0%", `${(i % 2 ? -1 : 1) * 12}%`, "0%"], y: ["0%", `${(i % 3 ? 1 : -1) * 14}%`, "0%"] }}
51
+ transition={{ duration: s.speed * (2 + i * 0.4), repeat: Infinity, ease: "easeInOut" }}
52
+ />
53
+ ))}
54
+ </motion.span>
55
+ {/* Glass highlight */}
56
+ <span className="absolute inset-0 rounded-full bg-[radial-gradient(circle_at_30%_25%,rgba(255,255,255,0.45),transparent_35%)]" />
57
+ <span className="absolute inset-0 rounded-full ring-1 ring-inset ring-white/15" />
58
+ </motion.span>
59
+ </button>
60
+ {showLabel && (
61
+ <div className="flex items-center gap-2 text-sm font-medium">
62
+ {state === "idle" ? <MicOff className="h-4 w-4 opacity-60" /> : <Mic className="h-4 w-4" style={{ color: colors[0] }} />}
63
+ <AnimatePresence mode="wait">
64
+ <motion.span key={state} initial={{ opacity: 0, y: 6 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -6 }} transition={{ duration: 0.18 }}>
65
+ {s.label}
66
+ </motion.span>
67
+ </AnimatePresence>
68
+ </div>
69
+ )}
70
+ </div>
71
+ );
72
+ };
73
+
74
+ export default AiVoiceOrb;
@@ -0,0 +1,120 @@
1
+ import React, { useEffect, useRef, useState } from "react";
2
+ import { AnimatePresence, motion } from "framer-motion";
3
+ import { Mic, MicOff } from "lucide-react";
4
+
5
+ /*
6
+ * AI Voice Ring: radial bars around a glowing core, like a circular audio
7
+ * visualiser. The ring breathes when idle, flickers while listening, spins
8
+ * while thinking and blooms while speaking. Tap to cycle, or set `state`.
9
+ */
10
+
11
+ const LABELS = { idle: "Tap to talk", listening: "Listening…", thinking: "Thinking…", speaking: "Speaking…" };
12
+ const ORDER = ["idle", "listening", "thinking", "speaking"];
13
+
14
+ const AiVoiceRing = ({ state: stateProp, onStateChange, colors = ["#a3e635", "#22d3ee"], bars = 72, size = 14, showLabel = true, className = "" }) => {
15
+ const canvasRef = useRef(null);
16
+ const [inner, setInner] = useState("idle");
17
+ const state = stateProp ?? inner;
18
+ const stateRef = useRef(state);
19
+ stateRef.current = state;
20
+
21
+ useEffect(() => {
22
+ const canvas = canvasRef.current;
23
+ const ctx = canvas.getContext("2d");
24
+ let raf = 0;
25
+ let spin = 0;
26
+ let last = performance.now();
27
+ const lens = Array(bars).fill(0.05);
28
+ const resize = () => {
29
+ const r = canvas.getBoundingClientRect();
30
+ const dpr = Math.min(window.devicePixelRatio || 1, 2);
31
+ canvas.width = Math.round(r.width * dpr);
32
+ canvas.height = Math.round(r.height * dpr);
33
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
34
+ };
35
+ const ro = new ResizeObserver(resize);
36
+ ro.observe(canvas);
37
+ resize();
38
+
39
+ const draw = (now) => {
40
+ const dt = Math.min(0.05, (now - last) / 1000);
41
+ last = now;
42
+ const t = now / 1000;
43
+ const s = stateRef.current;
44
+ const { width: w, height: h } = canvas.getBoundingClientRect();
45
+ const cx = w / 2;
46
+ const cy = h / 2;
47
+ const inner = Math.min(w, h) * 0.24;
48
+ const maxLen = Math.min(w, h) * 0.22;
49
+ spin += dt * (s === "thinking" ? 1.6 : 0.15);
50
+
51
+ ctx.clearRect(0, 0, w, h);
52
+ const core = ctx.createRadialGradient(cx, cy, 0, cx, cy, inner * 1.1);
53
+ core.addColorStop(0, `${colors[0]}66`);
54
+ core.addColorStop(1, `${colors[0]}00`);
55
+ ctx.fillStyle = core;
56
+ ctx.beginPath();
57
+ ctx.arc(cx, cy, inner * 1.1, 0, Math.PI * 2);
58
+ ctx.fill();
59
+
60
+ ctx.lineCap = "round";
61
+ ctx.lineWidth = Math.max(2, (Math.PI * 2 * inner) / bars * 0.45);
62
+ for (let i = 0; i < bars; i++) {
63
+ const a = (i / bars) * Math.PI * 2 + spin;
64
+ let target = 0.05 + 0.03 * Math.sin(t * 2 + i * 0.3);
65
+ if (s === "listening") target = 0.15 + 0.45 * Math.abs(Math.sin(t * 7 + i * 0.7) * Math.cos(t * 2.3 + i * 0.21));
66
+ if (s === "thinking") target = 0.12 + 0.3 * Math.max(0, Math.sin(i * 0.26 - t * 5));
67
+ if (s === "speaking") target = 0.2 + 0.8 * Math.abs(Math.sin(t * 8 + i * 0.45) * Math.sin(t * 2.6 + i * 0.13));
68
+ lens[i] += (target - lens[i]) * 0.2;
69
+ const len = lens[i] * maxLen;
70
+ const k = i / bars;
71
+ ctx.strokeStyle = k < 0.5 ? colors[0] : colors[1] ?? colors[0];
72
+ ctx.globalAlpha = 0.55 + lens[i] * 0.45;
73
+ ctx.beginPath();
74
+ ctx.moveTo(cx + Math.cos(a) * inner, cy + Math.sin(a) * inner);
75
+ ctx.lineTo(cx + Math.cos(a) * (inner + len), cy + Math.sin(a) * (inner + len));
76
+ ctx.stroke();
77
+ }
78
+ ctx.globalAlpha = 1;
79
+ ctx.fillStyle = colors[0];
80
+ ctx.shadowColor = colors[0];
81
+ ctx.shadowBlur = 20;
82
+ ctx.beginPath();
83
+ ctx.arc(cx, cy, inner * (0.32 + 0.06 * Math.sin(t * 3)), 0, Math.PI * 2);
84
+ ctx.fill();
85
+ ctx.shadowBlur = 0;
86
+ raf = requestAnimationFrame(draw);
87
+ };
88
+ raf = requestAnimationFrame(draw);
89
+ return () => {
90
+ cancelAnimationFrame(raf);
91
+ ro.disconnect();
92
+ };
93
+ }, [colors, bars]);
94
+
95
+ const next = () => {
96
+ const n = ORDER[(ORDER.indexOf(state) + 1) % ORDER.length];
97
+ if (stateProp === undefined) setInner(n);
98
+ onStateChange?.(n);
99
+ };
100
+
101
+ return (
102
+ <div className={`flex flex-col items-center gap-4 ${className}`}>
103
+ <button type="button" onClick={next} aria-label={`Voice assistant: ${LABELS[state]}`} className="rounded-full outline-none focus-visible:ring-2 focus-visible:ring-white/30" style={{ width: `min(${size}rem, 80vw)`, aspectRatio: "1" }}>
104
+ <canvas ref={canvasRef} aria-hidden="true" className="block h-full w-full" />
105
+ </button>
106
+ {showLabel && (
107
+ <div className="flex items-center gap-2 text-sm font-medium">
108
+ {state === "idle" ? <MicOff className="h-4 w-4 opacity-60" /> : <Mic className="h-4 w-4" style={{ color: colors[0] }} />}
109
+ <AnimatePresence mode="wait">
110
+ <motion.span key={state} initial={{ opacity: 0, y: 6 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -6 }} transition={{ duration: 0.18 }}>
111
+ {LABELS[state]}
112
+ </motion.span>
113
+ </AnimatePresence>
114
+ </div>
115
+ )}
116
+ </div>
117
+ );
118
+ };
119
+
120
+ export default AiVoiceRing;