apex-ui-kit 2.1.3 → 2.3.0

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 (64) 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/AnimatedButton/AnimatedButton.jsx +251 -0
  14. package/dist/ApexUI-Kit/templates/AnimatedSwitch/AnimatedSwitch.jsx +80 -0
  15. package/dist/ApexUI-Kit/templates/AnimatedTabs/AnimatedTabs.jsx +107 -0
  16. package/dist/ApexUI-Kit/templates/AuroraBackground/AuroraBackground.jsx +48 -0
  17. package/dist/ApexUI-Kit/templates/BlurRevealText/BlurRevealText.jsx +60 -0
  18. package/dist/ApexUI-Kit/templates/BounceCards/BounceCards.jsx +53 -0
  19. package/dist/ApexUI-Kit/templates/CardSwap3D/CardSwap3D.jsx +80 -0
  20. package/dist/ApexUI-Kit/templates/Carousel3D/Carousel3D.jsx +222 -0
  21. package/dist/ApexUI-Kit/templates/CircularText/CircularText.jsx +49 -0
  22. package/dist/ApexUI-Kit/templates/ClickSpark/ClickSpark.jsx +72 -0
  23. package/dist/ApexUI-Kit/templates/Coverflow3D/Coverflow3D.jsx +108 -0
  24. package/dist/ApexUI-Kit/templates/CubeGallery/CubeGallery.jsx +166 -0
  25. package/dist/ApexUI-Kit/templates/CursorTrail/CursorTrail.jsx +200 -0
  26. package/dist/ApexUI-Kit/templates/DotGlobe/DotGlobe.jsx +289 -0
  27. package/dist/ApexUI-Kit/templates/ElasticSlider/ElasticSlider.jsx +152 -0
  28. package/dist/ApexUI-Kit/templates/ExtrudedText/ExtrudedText.jsx +78 -0
  29. package/dist/ApexUI-Kit/templates/FaqAccordion/FaqAccordion.jsx +88 -0
  30. package/dist/ApexUI-Kit/templates/FlipBook/FlipBook.jsx +191 -0
  31. package/dist/ApexUI-Kit/templates/FlipClock/FlipClock.jsx +121 -0
  32. package/dist/ApexUI-Kit/templates/FloatingNavbar/FloatingNavbar.jsx +162 -0
  33. package/dist/ApexUI-Kit/templates/Folder/Folder.jsx +101 -0
  34. package/dist/ApexUI-Kit/templates/GlitchText/GlitchText.jsx +56 -0
  35. package/dist/ApexUI-Kit/templates/GooeyNav/GooeyNav.jsx +310 -0
  36. package/dist/ApexUI-Kit/templates/HoldToConfirm/HoldToConfirm.jsx +169 -0
  37. package/dist/ApexUI-Kit/templates/HoldToConfirm/UndoNotice.jsx +58 -0
  38. package/dist/ApexUI-Kit/templates/HoloCard/HoloCard.jsx +92 -0
  39. package/dist/ApexUI-Kit/templates/ImageCompare/ImageCompare.jsx +94 -0
  40. package/dist/ApexUI-Kit/templates/IsoStack/IsoStack.jsx +86 -0
  41. package/dist/ApexUI-Kit/templates/LiquidGradient/LiquidGradient.jsx +238 -0
  42. package/dist/ApexUI-Kit/templates/LoadingScreen/LoadingScreen.jsx +805 -0
  43. package/dist/ApexUI-Kit/templates/MagnetLines/MagnetLines.jsx +67 -0
  44. package/dist/ApexUI-Kit/templates/MagneticCursor/MagneticCursor.jsx +148 -0
  45. package/dist/ApexUI-Kit/templates/PixelTransition/PixelTransition.jsx +75 -0
  46. package/dist/ApexUI-Kit/templates/PricingCards/PricingCards.jsx +115 -0
  47. package/dist/ApexUI-Kit/templates/ProfileCard/ProfileCard.jsx +213 -0
  48. package/dist/ApexUI-Kit/templates/ProximityText/ProximityText.jsx +66 -0
  49. package/dist/ApexUI-Kit/templates/RotatingText/RotatingText.jsx +50 -0
  50. package/dist/ApexUI-Kit/templates/ShinyText/ShinyText.jsx +30 -0
  51. package/dist/ApexUI-Kit/templates/SidebarNav/SidebarNav.jsx +220 -0
  52. package/dist/ApexUI-Kit/templates/SpotlightGrid/SpotlightGrid.jsx +110 -0
  53. package/dist/ApexUI-Kit/templates/StarBorder/StarBorder.jsx +33 -0
  54. package/dist/ApexUI-Kit/templates/Stepper/Stepper.jsx +129 -0
  55. package/dist/ApexUI-Kit/templates/SwipeCards/SwipeCards.jsx +123 -0
  56. package/dist/ApexUI-Kit/templates/TextRing3D/TextRing3D.jsx +73 -0
  57. package/dist/ApexUI-Kit/templates/ToastStack/ToastStack.jsx +108 -0
  58. package/dist/ApexUI-Kit/templates/ToolTip/ToolTip.jsx +5 -3
  59. package/dist/ApexUI-Kit/templates/TrueFocus/TrueFocus.jsx +64 -0
  60. package/dist/ApexUI-Kit/templates/TypewriterText/TypewriterText.jsx +70 -0
  61. package/dist/ApexUI-Kit/templates/WebGLCarousel/WebGLCarousel.jsx +326 -0
  62. package/dist/ApexUI-Kit/templates/WorldMap/WorldMap.jsx +359 -0
  63. package/dist/cli/index.js +63 -0
  64. package/package.json +1 -1
@@ -0,0 +1,166 @@
1
+ import React, { useEffect, useRef, useState } from "react";
2
+ import { animate, motion, useMotionValue, useMotionValueEvent, useReducedMotion } from "framer-motion";
3
+ import { ChevronLeft, ChevronRight } from "lucide-react";
4
+
5
+ /*
6
+ * Cube Gallery: up to six images on a solid 3D cube seen from a corner, so
7
+ * three faces show at once. It spins slowly on its own; drag in any
8
+ * direction to throw it around, and it eases back to a corner view. Faces
9
+ * are shaded by how much they turn away from you, which sells the depth.
10
+ * Arrows / arrow keys turn it a quarter at a time.
11
+ */
12
+
13
+ const IK = "https://ik.imagekit.io/ny6yinyut/apexui/assets";
14
+ const img = (n) => `${IK}/${n}.png?tr=f-auto,q-80,w-600`;
15
+ const DEFAULT_FACES = [
16
+ { image: img("spiderman"), title: "Spider-Man" },
17
+ { image: img("black-panther"), title: "Black Panther" },
18
+ { image: img("hulk"), title: "Hulk" },
19
+ { image: img("thanos"), title: "Thanos" },
20
+ { image: img("captainamerica"), title: "Captain America" },
21
+ { image: img("doctorStrange"), title: "Doctor Strange" },
22
+ ];
23
+
24
+ // Side faces (around Y) then top and bottom.
25
+ const SIDES = [
26
+ { yaw: 0, pitch: 0 },
27
+ { yaw: 90, pitch: 0 },
28
+ { yaw: 180, pitch: 0 },
29
+ { yaw: 270, pitch: 0 },
30
+ { yaw: 0, pitch: 90 },
31
+ { yaw: 0, pitch: -90 },
32
+ ];
33
+ const RAD = Math.PI / 180;
34
+
35
+ const CubeGallery = ({ faces = DEFAULT_FACES, size = 13, autoRotate = true, speed = 12, tilt = -24, accentColor = "#a3e635", className = "" }) => {
36
+ const reduced = useReducedMotion();
37
+ const rotY = useMotionValue(-35);
38
+ const rotX = useMotionValue(tilt);
39
+ const shades = useRef([]);
40
+ const drag = useRef(null);
41
+ const [hover, setHover] = useState(false);
42
+ const [index, setIndex] = useState(0);
43
+ const half = `${size / 2}rem`;
44
+
45
+ // Darken each face by how far it turns from the viewer.
46
+ const shade = () => {
47
+ const y = rotY.get();
48
+ const x = rotX.get();
49
+ SIDES.forEach((s, i) => {
50
+ const el = shades.current[i];
51
+ if (!el) return;
52
+ let facing;
53
+ if (s.pitch === 0) facing = Math.cos((s.yaw + y) * RAD) * Math.cos(x * RAD);
54
+ else facing = Math.sin((s.pitch > 0 ? -x : x) * RAD);
55
+ el.style.opacity = String(0.08 + (1 - Math.max(0, facing)) * 0.62);
56
+ });
57
+ };
58
+ useMotionValueEvent(rotY, "change", shade);
59
+ useMotionValueEvent(rotX, "change", shade);
60
+ useEffect(shade);
61
+
62
+ useEffect(() => {
63
+ rotX.set(tilt);
64
+ }, [tilt, rotX]);
65
+
66
+ // Slow continuous spin while idle.
67
+ useEffect(() => {
68
+ if (!autoRotate || hover || reduced) return undefined;
69
+ let raf = 0;
70
+ let last = performance.now();
71
+ const tick = (now) => {
72
+ if (!drag.current) rotY.set(rotY.get() - ((now - last) / 1000) * speed);
73
+ last = now;
74
+ raf = requestAnimationFrame(tick);
75
+ };
76
+ raf = requestAnimationFrame(tick);
77
+ return () => cancelAnimationFrame(raf);
78
+ }, [autoRotate, hover, reduced, speed, rotY]);
79
+
80
+ const turn = (dir) => {
81
+ // Land on the next corner view, 90° along.
82
+ const target = Math.round((rotY.get() + 35) / 90) * 90 - 35 - dir * 90;
83
+ animate(rotY, target, { type: "spring", stiffness: 110, damping: 18 });
84
+ animate(rotX, tilt, { type: "spring", stiffness: 110, damping: 18 });
85
+ setIndex((i) => (((i + dir) % 4) + 4) % 4);
86
+ };
87
+
88
+ return (
89
+ <div
90
+ className={`flex flex-col items-center gap-12 outline-none ${className}`}
91
+ tabIndex={0}
92
+ onKeyDown={(e) => {
93
+ if (e.key === "ArrowRight") turn(1);
94
+ if (e.key === "ArrowLeft") turn(-1);
95
+ }}
96
+ onPointerEnter={() => setHover(true)}
97
+ onPointerLeave={() => setHover(false)}
98
+ aria-roledescription="carousel"
99
+ aria-label="Cube gallery"
100
+ >
101
+ <div className="relative [perspective:1100px]" style={{ width: `${size}rem`, height: `${size}rem`, maxWidth: "60vw", maxHeight: "60vw", margin: `${size * 0.16}rem 0` }}>
102
+ {/* Soft floor shadow */}
103
+ <div className="pointer-events-none absolute -bottom-[22%] left-1/2 h-[18%] w-[95%] -translate-x-1/2 rounded-[50%] bg-black/40 blur-xl" />
104
+ <motion.div
105
+ className="relative h-full w-full cursor-grab touch-none active:cursor-grabbing [transform-style:preserve-3d]"
106
+ style={{ rotateX: rotX, rotateY: rotY }}
107
+ onPointerDown={(e) => {
108
+ e.currentTarget.setPointerCapture(e.pointerId);
109
+ rotY.stop();
110
+ rotX.stop();
111
+ drag.current = { x: e.clientX, y: e.clientY, sx: rotY.get(), sy: rotX.get(), vx: 0, vy: 0, t: performance.now(), lx: e.clientX, ly: e.clientY };
112
+ }}
113
+ onPointerMove={(e) => {
114
+ const d = drag.current;
115
+ if (!d) return;
116
+ rotY.set(d.sx + (e.clientX - d.x) * 0.5);
117
+ rotX.set(Math.max(-70, Math.min(70, d.sy - (e.clientY - d.y) * 0.4)));
118
+ const now = performance.now();
119
+ const dt = Math.max(1, now - d.t);
120
+ d.vx = ((e.clientX - d.lx) / dt) * 0.5 * 1000;
121
+ d.vy = ((e.clientY - d.ly) / dt) * 0.4 * 1000;
122
+ d.t = now;
123
+ d.lx = e.clientX;
124
+ d.ly = e.clientY;
125
+ }}
126
+ onPointerUp={() => {
127
+ const d = drag.current;
128
+ if (!d) return;
129
+ drag.current = null;
130
+ // Throw with momentum, then settle back to the corner tilt.
131
+ animate(rotY, rotY.get() + d.vx * 0.35, { type: "spring", stiffness: 40, damping: 16 });
132
+ animate(rotX, tilt, { type: "spring", stiffness: 70, damping: 14 });
133
+ }}
134
+ >
135
+ {SIDES.map((s, i) => {
136
+ const f = faces[i % faces.length];
137
+ const transform = s.pitch ? `rotateX(${s.pitch}deg) translateZ(${half})` : `rotateY(${s.yaw}deg) translateZ(${half})`;
138
+ return (
139
+ <figure key={i} className="absolute inset-0 m-0 overflow-hidden bg-neutral-900 [backface-visibility:hidden]" style={{ transform, boxShadow: "inset 0 0 0 1px rgba(255,255,255,0.14)" }}>
140
+ <img src={f.image} alt={f.title} draggable="false" className="h-full w-full select-none object-cover" />
141
+ <figcaption className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/75 to-transparent px-3 pb-2.5 pt-8 text-[0.8rem] font-semibold text-white">{f.title}</figcaption>
142
+ <span ref={(el) => (shades.current[i] = el)} className="pointer-events-none absolute inset-0 bg-black" style={{ opacity: 0.3 }} />
143
+ </figure>
144
+ );
145
+ })}
146
+ </motion.div>
147
+ </div>
148
+
149
+ <div className="flex items-center gap-4">
150
+ <button type="button" aria-label="Turn left" onClick={() => turn(-1)} className="grid h-10 w-10 place-items-center rounded-full border border-current/20 transition hover:scale-105 active:scale-95">
151
+ <ChevronLeft className="h-5 w-5" />
152
+ </button>
153
+ <div className="flex gap-1.5" aria-hidden="true">
154
+ {[0, 1, 2, 3].map((i) => (
155
+ <span key={i} className="h-2 rounded-full transition-all duration-300" style={{ width: i === index ? 22 : 8, background: i === index ? accentColor : "color-mix(in srgb, currentColor 25%, transparent)" }} />
156
+ ))}
157
+ </div>
158
+ <button type="button" aria-label="Turn right" onClick={() => turn(1)} className="grid h-10 w-10 place-items-center rounded-full border border-current/20 transition hover:scale-105 active:scale-95">
159
+ <ChevronRight className="h-5 w-5" />
160
+ </button>
161
+ </div>
162
+ </div>
163
+ );
164
+ };
165
+
166
+ export default CubeGallery;
@@ -0,0 +1,200 @@
1
+ import React, { useEffect, useRef } from "react";
2
+
3
+ /*
4
+ * Cursor Trail: a tail that follows the mouse — comet, rainbow, dots, ribbon,
5
+ * sparkles or ghost rings. Wrap an area with it, or pass `global` to follow
6
+ * the pointer across the whole page. Everything is drawn on one canvas and
7
+ * the loop sleeps as soon as the tail has faded. Touch input is ignored.
8
+ */
9
+
10
+ const hexToRgb = (hex) => {
11
+ let h = String(hex).replace("#", "");
12
+ if (h.length === 3) h = h.split("").map((c) => c + c).join("");
13
+ const n = parseInt(h, 16) || 0;
14
+ return `${(n >> 16) & 255},${(n >> 8) & 255},${n & 255}`;
15
+ };
16
+
17
+ const CursorTrail = ({ children, variant = "comet", color = "#a3e635", size = 1, length = 1, global = false, className = "" }) => {
18
+ const wrapRef = useRef(null);
19
+ const canvasRef = useRef(null);
20
+
21
+ useEffect(() => {
22
+ const canvas = canvasRef.current;
23
+ const target = global ? window : wrapRef.current;
24
+ if (!canvas || !target || window.matchMedia("(prefers-reduced-motion: reduce)").matches) return undefined;
25
+ const ctx = canvas.getContext("2d");
26
+ const rgb = hexToRgb(color);
27
+ const s = { points: [], chain: [], parts: [], rings: [], last: null, raf: 0, spawn: 0 };
28
+ const life = 320 * length;
29
+
30
+ const resize = () => {
31
+ const r = canvas.getBoundingClientRect();
32
+ const dpr = Math.min(window.devicePixelRatio || 1, 2);
33
+ canvas.width = Math.round(r.width * dpr);
34
+ canvas.height = Math.round(r.height * dpr);
35
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
36
+ };
37
+ const ro = new ResizeObserver(resize);
38
+ ro.observe(canvas);
39
+ resize();
40
+
41
+ // Points that each ease toward the one ahead: used by dots and ribbon.
42
+ const follow = (count, ease) => {
43
+ if (!s.last) return false;
44
+ if (s.chain.length !== count) s.chain = Array.from({ length: count }, () => ({ x: s.last.x, y: s.last.y }));
45
+ let tx = s.last.x;
46
+ let ty = s.last.y;
47
+ for (const p of s.chain) {
48
+ p.x += (tx - p.x) * ease;
49
+ p.y += (ty - p.y) * ease;
50
+ tx = p.x;
51
+ ty = p.y;
52
+ }
53
+ const tail = s.chain[s.chain.length - 1];
54
+ return Math.hypot(tail.x - s.last.x, tail.y - s.last.y) > 0.4;
55
+ };
56
+
57
+ const frame = () => {
58
+ const now = performance.now();
59
+ const { width, height } = canvas.getBoundingClientRect();
60
+ ctx.clearRect(0, 0, width, height);
61
+ let alive = false;
62
+
63
+ if (variant === "comet" || variant === "rainbow") {
64
+ s.points = s.points.filter((p) => now - p.t < life);
65
+ const n = s.points.length;
66
+ alive = n > 1;
67
+ ctx.lineCap = "round";
68
+ for (let i = 1; i < n; i++) {
69
+ const k = i / n;
70
+ const c = variant === "rainbow" ? `hsla(${(now / 6 + i * 9) % 360},90%,62%,${k})` : `rgba(${rgb},${k * 0.9})`;
71
+ ctx.strokeStyle = c;
72
+ ctx.shadowColor = c;
73
+ ctx.shadowBlur = 12 * size;
74
+ ctx.lineWidth = (1 + k * 7) * size;
75
+ ctx.beginPath();
76
+ ctx.moveTo(s.points[i - 1].x, s.points[i - 1].y);
77
+ ctx.lineTo(s.points[i].x, s.points[i].y);
78
+ ctx.stroke();
79
+ }
80
+ ctx.shadowBlur = 0;
81
+ }
82
+
83
+ if (variant === "dots") {
84
+ alive = follow(Math.round(14 * length), 0.38);
85
+ s.chain.forEach((d, i) => {
86
+ const k = 1 - i / s.chain.length;
87
+ ctx.fillStyle = `rgba(${rgb},${k * 0.85})`;
88
+ ctx.beginPath();
89
+ ctx.arc(d.x, d.y, (1.5 + k * 4) * size, 0, Math.PI * 2);
90
+ ctx.fill();
91
+ });
92
+ }
93
+
94
+ if (variant === "ribbon") {
95
+ alive = follow(Math.round(22 * length), 0.42);
96
+ const pts = s.last ? [s.last, ...s.chain] : [];
97
+ ctx.lineCap = "round";
98
+ ctx.lineJoin = "round";
99
+ for (let i = 1; i < pts.length - 1; i++) {
100
+ const k = 1 - i / pts.length;
101
+ const mx1 = (pts[i - 1].x + pts[i].x) / 2;
102
+ const my1 = (pts[i - 1].y + pts[i].y) / 2;
103
+ const mx2 = (pts[i].x + pts[i + 1].x) / 2;
104
+ const my2 = (pts[i].y + pts[i + 1].y) / 2;
105
+ ctx.strokeStyle = `rgba(${rgb},${0.15 + k * 0.85})`;
106
+ ctx.lineWidth = (2 + k * 12) * size;
107
+ ctx.beginPath();
108
+ ctx.moveTo(mx1, my1);
109
+ ctx.quadraticCurveTo(pts[i].x, pts[i].y, mx2, my2);
110
+ ctx.stroke();
111
+ }
112
+ }
113
+
114
+ if (variant === "sparkles") {
115
+ s.parts = s.parts.filter((p) => now - p.born < p.life);
116
+ for (const p of s.parts) {
117
+ const k = 1 - (now - p.born) / p.life;
118
+ p.x += p.vx;
119
+ p.y += p.vy;
120
+ p.vy += 0.05;
121
+ p.vx *= 0.97;
122
+ const r = p.size * k * size;
123
+ ctx.strokeStyle = p.white ? `rgba(255,255,255,${k})` : `rgba(${rgb},${k})`;
124
+ ctx.lineWidth = 1.4;
125
+ ctx.beginPath();
126
+ ctx.moveTo(p.x - r, p.y);
127
+ ctx.lineTo(p.x + r, p.y);
128
+ ctx.moveTo(p.x, p.y - r);
129
+ ctx.lineTo(p.x, p.y + r);
130
+ ctx.stroke();
131
+ }
132
+ alive = s.parts.length > 0;
133
+ }
134
+
135
+ if (variant === "ghost") {
136
+ const span = 650 * length;
137
+ s.rings = s.rings.filter((r) => now - r.born < span);
138
+ for (const r of s.rings) {
139
+ const k = (now - r.born) / span;
140
+ ctx.strokeStyle = `rgba(${rgb},${(1 - k) * 0.7})`;
141
+ ctx.lineWidth = (2 * (1 - k) + 0.5) * size;
142
+ ctx.beginPath();
143
+ ctx.arc(r.x, r.y, (6 + k * 22) * size, 0, Math.PI * 2);
144
+ ctx.stroke();
145
+ }
146
+ alive = s.rings.length > 0;
147
+ }
148
+
149
+ s.raf = alive ? requestAnimationFrame(frame) : 0;
150
+ };
151
+
152
+ const onMove = (e) => {
153
+ if (e.pointerType && e.pointerType !== "mouse") return;
154
+ const r = canvas.getBoundingClientRect();
155
+ const x = e.clientX - r.left;
156
+ const y = e.clientY - r.top;
157
+ const now = performance.now();
158
+ const prev = s.last;
159
+ s.last = { x, y };
160
+ if (variant === "comet" || variant === "rainbow") s.points.push({ x, y, t: now });
161
+ if (variant === "sparkles" && prev) {
162
+ const speed = Math.hypot(x - prev.x, y - prev.y);
163
+ for (let i = 0; i < Math.min(4, Math.ceil(speed / 8)); i++) {
164
+ s.parts.push({ x, y, vx: (Math.random() - 0.5) * 2.4, vy: (Math.random() - 0.7) * 2.4, size: 2 + Math.random() * 3, life: (500 + Math.random() * 400) * length, born: now, white: Math.random() < 0.3 });
165
+ }
166
+ }
167
+ if (variant === "ghost" && now - s.spawn > 70) {
168
+ s.spawn = now;
169
+ s.rings.push({ x, y, born: now });
170
+ }
171
+ if (!s.raf) s.raf = requestAnimationFrame(frame);
172
+ };
173
+
174
+ target.addEventListener("pointermove", onMove);
175
+ return () => {
176
+ target.removeEventListener("pointermove", onMove);
177
+ cancelAnimationFrame(s.raf);
178
+ ro.disconnect();
179
+ };
180
+ }, [variant, color, size, length, global]);
181
+
182
+ const canvas = <canvas ref={canvasRef} aria-hidden="true" className={`pointer-events-none ${global ? "fixed inset-0 z-[9999]" : "absolute inset-0 z-40"} h-full w-full`} />;
183
+
184
+ if (global) {
185
+ return (
186
+ <>
187
+ {children}
188
+ {canvas}
189
+ </>
190
+ );
191
+ }
192
+ return (
193
+ <div ref={wrapRef} className={`relative ${className}`}>
194
+ {children}
195
+ {canvas}
196
+ </div>
197
+ );
198
+ };
199
+
200
+ export default CursorTrail;
@@ -0,0 +1,289 @@
1
+ import React, { useEffect, useRef } from "react";
2
+
3
+ /*
4
+ * Dot Globe: a 3D globe of land dots drawn on a 2D canvas (no WebGL, no map
5
+ * library). Drag to spin it with inertia; it also turns on its own. Cities
6
+ * pulse as markers and flights arc between them.
7
+ */
8
+
9
+ // 2° land mask (Natural Earth 110m): 180 × 69 cells from 180°W, 78°N, one bit each.
10
+ const LAND = "AAAAwADkw////wEABAAAAABgAAAAAAAAAADAUT8A/v8PAAAAAAwA/j8AuAEAAAAAcBHtDcD/fwAAAAAwAPz/fwMAABCAAQD5w/wD8P8FAAAcAIL7//9//xOAAP/ff0668QD/HwAA+A8At///////v3/w/////x8++B8AAOD/1//7////////jP////+/+AE/gAcAn9f//////////w/w/////wEs4AEAAHz+////////////gL////8HeAAcAADg5///////////9ADgAf7/f4AnAAAAAH78////////H0QAAAiA//8f8AcAAIBB4////////38ADwAQAOD//5//AQAAHAT/////////A3AAAAAA/v//+T8AAGDz//////////8DAQAAAMD/////AwAAsP//////////LwAAAAAA6P///2IAAAD+//////////8CAAAAAAD///8/CAAA4P//////////JwAAAAAA8P///wYAAAD+/unz/////z8AAAAAAAD///8HAAAA/pgPPP//////MQAAAAAA8P//PwAAAMBD9v7n/////wcBAAAAAAD///8AAAAAPkD7f/7///8hEAAAAAAA4P//DwAAAIDhAv/n////f8YAAAAAAAD8//8AAAAA+AdE//////8jDwAAAAAAgP//AwAAAMD/APD/////PxgAAAAAAADw/x8AAAAA/n/v//////8HAAAAAAAAAPwDAgAAAOD////7////fwAAAAAAAACgHyAAAACA//9/f/7///8DAAAAAAAAAPQBAAAAAPj//+cv+P//PwAAAAAAAAAAHjAAAADA/////g/+//8EAAAAAAAAAOBhCAAAAP7//99/4D//AAAAAAAAAAAAPAMEAADA////+Qf84BcAAAAAAAAAAAA/AAAAAPz//58fgAf+QAAAAAAAAAAAAA8AAADg////ewA4gA8EAAAAAAAAAADAAAAAAPz//38BgAP4QQAAAAAAAAAAAAgPAADA////zwAwgAwQAAAAAAAAAAAA9Q8AAPj///8HAAVIAAAAAAAAAAAAAID/AQAA////fwBAAAAQAAAAAAAAAAAA+P8AAGDh//8DAAA0GAAAAAAAAAAAAID/HwAAAPj/HwAAgMIBAAAAAAAAAAAA/P8BAACA//8AAAAYXgAAAAAAAAAAAMD/fwAAAPz/BwAAAOOBAQAAAAAAAAAA/P8/AACA/z8AAABgbtQBAAAAAAAAAOD//w8AAPD/AwAAAAQIeAAAAAAAAAAA/P//AQAA/z8AAACAA4APAQAAAAAAAID//w8AAPD/AwAAAAARsEAAAAAAAAAA+P9/AAAA/j8AAAAAAAAAAAAAAAAAAAD//wcAAPD/QwAAAACAIwAAAAAAAAAA8P9/AAAA/z8EAAAAAD8GIAAAAAAAAAD8/wMAAPD/cQAAAAD4ZwAAAAAAAAAAgP8/AAAA/w8HAAAAgP8HAAAAAAAAAAD4/wMAAOD/MAAAAAD//wEBAAAAAAAAgP8PAAAA/g8DAAAA+P8fAAAAAAAAAAD4PwAAAOB/EAAAAID//wMAAAAAAAAAgP8DAAAA/AMAAAAA+P9/AAAAAAAAAAD8HwAAAMA/AAAAAID//wcAAAAAAAAAwP8BAAAA+AEAAAAA8P9/AAAAAAAAAAD8DwAAAIAPAAAAAAAP/gMAAAAAAAAAwB8AAAAAAAAAAAAAEIAfAAEAAAAAAAD+AwAAAAAAAAAAAAAA8AEgAAAAAAAA4AcAAAAAAAAAAAAAAAAAAAYAAAAAAABeAAAAAAAAAAAAAAAAwAAwAAAAAAAAwAMAAAAAAAAAAAAAAAAIgAEAAAAAAAAeAAAAAAAAAAAAAAAAAAAMAAAAAAAA4AEAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAAAAAAABAAAAAAAAAAAAAAAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
11
+ const STEP = 2;
12
+ const COLS = 180;
13
+ const ROWS = 69;
14
+
15
+ const landDots = (() => {
16
+ const bin = typeof atob === "function" ? atob(LAND) : "";
17
+ const dots = [];
18
+ for (let r = 0; r < ROWS; r++) {
19
+ const lat = 78 - r * STEP - 1;
20
+ // Fewer dots toward the poles so the globe looks evenly filled.
21
+ const skip = Math.max(1, Math.round(1 / Math.max(0.25, Math.cos((lat * Math.PI) / 180))));
22
+ for (let c = 0; c < COLS; c += skip) {
23
+ const i = r * COLS + c;
24
+ if ((bin.charCodeAt(i >> 3) >> (i & 7)) & 1) dots.push([lat, -180 + c * STEP + 1]);
25
+ }
26
+ }
27
+ return dots;
28
+ })();
29
+
30
+ const DEFAULT_MARKERS = [
31
+ { label: "Bhubaneswar", lat: 20.3, lon: 85.8 },
32
+ { label: "San Francisco", lat: 37.8, lon: -122.4 },
33
+ { label: "London", lat: 51.5, lon: -0.1 },
34
+ { label: "Tokyo", lat: 35.7, lon: 139.7 },
35
+ { label: "Sydney", lat: -33.9, lon: 151.2 },
36
+ { label: "São Paulo", lat: -23.5, lon: -46.6 },
37
+ { label: "Dubai", lat: 25.2, lon: 55.3 },
38
+ ];
39
+
40
+ const DEFAULT_ARCS = [
41
+ [0, 1],
42
+ [0, 2],
43
+ [0, 3],
44
+ [0, 4],
45
+ [2, 5],
46
+ [6, 2],
47
+ [3, 1],
48
+ ];
49
+
50
+ const rad = (d) => (d * Math.PI) / 180;
51
+ const toVec = (lat, lon) => [Math.cos(rad(lat)) * Math.sin(rad(lon)), Math.sin(rad(lat)), Math.cos(rad(lat)) * Math.cos(rad(lon))];
52
+
53
+ // Spherical interpolation between two unit vectors.
54
+ const slerp = (a, b, t) => {
55
+ const dot = Math.min(1, Math.max(-1, a[0] * b[0] + a[1] * b[1] + a[2] * b[2]));
56
+ const om = Math.acos(dot);
57
+ if (om < 1e-4) return a;
58
+ const s = Math.sin(om);
59
+ const k1 = Math.sin((1 - t) * om) / s;
60
+ const k2 = Math.sin(t * om) / s;
61
+ return [a[0] * k1 + b[0] * k2, a[1] * k1 + b[1] * k2, a[2] * k1 + b[2] * k2];
62
+ };
63
+
64
+ const DotGlobe = ({
65
+ markers = DEFAULT_MARKERS,
66
+ arcs = DEFAULT_ARCS,
67
+ dotColor = "#a1a1aa",
68
+ markerColor = "#a3e635",
69
+ arcColor = "#a3e635",
70
+ glowColor = "#a3e635",
71
+ labelColor = "rgba(255,255,255,0.9)",
72
+ autoRotateSpeed = 8,
73
+ tilt = 18,
74
+ maxSize = 30,
75
+ showLabels = true,
76
+ className = "",
77
+ }) => {
78
+ const wrapRef = useRef(null);
79
+ const canvasRef = useRef(null);
80
+ const opts = useRef(null);
81
+ opts.current = { markers, arcs, dotColor, markerColor, arcColor, glowColor, labelColor, autoRotateSpeed, tilt, showLabels };
82
+
83
+ useEffect(() => {
84
+ const canvas = canvasRef.current;
85
+ const wrap = wrapRef.current;
86
+ const ctx = canvas.getContext("2d");
87
+ const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
88
+ let size = 300;
89
+ let raf = 0;
90
+ let last = performance.now();
91
+ let time = 0;
92
+ let yaw = -80; // start over India
93
+ let velocity = 0;
94
+ let drag = null;
95
+ let visible = true;
96
+
97
+ const resize = () => {
98
+ const dpr = Math.min(window.devicePixelRatio || 1, 2);
99
+ size = wrap.getBoundingClientRect().width;
100
+ canvas.width = Math.round(size * dpr);
101
+ canvas.height = Math.round(size * dpr);
102
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
103
+ draw();
104
+ };
105
+
106
+ // Rotate a unit vector by the current yaw and tilt, then project.
107
+ const project = (v, R, c) => {
108
+ const cy = Math.cos(rad(yaw));
109
+ const sy = Math.sin(rad(yaw));
110
+ const x = v[0] * cy + v[2] * sy;
111
+ const z0 = -v[0] * sy + v[2] * cy;
112
+ const ct = Math.cos(rad(opts.current.tilt));
113
+ const st = Math.sin(rad(opts.current.tilt));
114
+ const y = v[1] * ct - z0 * st;
115
+ const z = v[1] * st + z0 * ct;
116
+ return [c + x * R, c - y * R, z];
117
+ };
118
+
119
+ function draw() {
120
+ const o = opts.current;
121
+ const c = size / 2;
122
+ const R = size * 0.4;
123
+ ctx.clearRect(0, 0, size, size);
124
+
125
+ // Atmosphere glow and sphere body.
126
+ const glow = ctx.createRadialGradient(c, c, R * 0.85, c, c, R * 1.25);
127
+ glow.addColorStop(0, `${o.glowColor}33`);
128
+ glow.addColorStop(1, `${o.glowColor}00`);
129
+ ctx.fillStyle = glow;
130
+ ctx.beginPath();
131
+ ctx.arc(c, c, R * 1.25, 0, Math.PI * 2);
132
+ ctx.fill();
133
+ const body = ctx.createRadialGradient(c - R * 0.35, c - R * 0.35, R * 0.1, c, c, R);
134
+ body.addColorStop(0, "rgba(255,255,255,0.07)");
135
+ body.addColorStop(1, "rgba(255,255,255,0.01)");
136
+ ctx.fillStyle = body;
137
+ ctx.beginPath();
138
+ ctx.arc(c, c, R, 0, Math.PI * 2);
139
+ ctx.fill();
140
+
141
+ // Land dots: front bright, back faint.
142
+ const dr = Math.max(0.8, R / 190);
143
+ ctx.fillStyle = o.dotColor;
144
+ for (let i = 0; i < landDots.length; i++) {
145
+ const [lat, lon] = landDots[i];
146
+ const [x, y, z] = project(toVec(lat, lon), R, c);
147
+ ctx.globalAlpha = z > 0 ? 0.35 + 0.65 * z : 0.06;
148
+ ctx.fillRect(x - dr, y - dr, dr * 2, dr * 2);
149
+ }
150
+ ctx.globalAlpha = 1;
151
+
152
+ // Arcs with a travelling head.
153
+ o.arcs.forEach(([a, b], k) => {
154
+ const A = o.markers[a];
155
+ const B = o.markers[b];
156
+ if (!A || !B) return;
157
+ const va = toVec(A.lat, A.lon);
158
+ const vb = toVec(B.lat, B.lon);
159
+ const steps = 48;
160
+ ctx.strokeStyle = o.arcColor;
161
+ ctx.lineWidth = Math.max(1, R / 220);
162
+ ctx.globalAlpha = 0.55;
163
+ ctx.beginPath();
164
+ let drawing = false;
165
+ for (let s = 0; s <= steps; s++) {
166
+ const t = s / steps;
167
+ const v = slerp(va, vb, t);
168
+ const lift = 1 + 0.18 * Math.sin(Math.PI * t);
169
+ const [x, y, z] = project([v[0] * lift, v[1] * lift, v[2] * lift], R, c);
170
+ if (z > -0.1) {
171
+ if (drawing) ctx.lineTo(x, y);
172
+ else ctx.moveTo(x, y);
173
+ drawing = true;
174
+ } else drawing = false;
175
+ }
176
+ ctx.stroke();
177
+ const t = ((time * 0.35 + k * 0.17) % 1 + 1) % 1;
178
+ const v = slerp(va, vb, t);
179
+ const lift = 1 + 0.18 * Math.sin(Math.PI * t);
180
+ const [hx, hy, hz] = project([v[0] * lift, v[1] * lift, v[2] * lift], R, c);
181
+ if (hz > 0) {
182
+ ctx.globalAlpha = 1;
183
+ ctx.fillStyle = o.arcColor;
184
+ ctx.beginPath();
185
+ ctx.arc(hx, hy, Math.max(1.5, R / 110), 0, Math.PI * 2);
186
+ ctx.fill();
187
+ }
188
+ });
189
+ ctx.globalAlpha = 1;
190
+
191
+ // Markers with a pulse and labels.
192
+ ctx.font = `600 ${Math.max(10, R / 18)}px ui-sans-serif, system-ui, sans-serif`;
193
+ o.markers.forEach((m, k) => {
194
+ const [x, y, z] = project(toVec(m.lat, m.lon), R, c);
195
+ if (z <= 0.05) return;
196
+ const pulse = ((time * 0.8 + k * 0.23) % 1 + 1) % 1;
197
+ ctx.strokeStyle = o.markerColor;
198
+ ctx.globalAlpha = (1 - pulse) * z;
199
+ ctx.lineWidth = 1.5;
200
+ ctx.beginPath();
201
+ ctx.arc(x, y, 3 + pulse * R * 0.06, 0, Math.PI * 2);
202
+ ctx.stroke();
203
+ ctx.globalAlpha = z;
204
+ ctx.fillStyle = o.markerColor;
205
+ ctx.beginPath();
206
+ ctx.arc(x, y, Math.max(2.5, R / 80), 0, Math.PI * 2);
207
+ ctx.fill();
208
+ if (o.showLabels && z > 0.35) {
209
+ ctx.globalAlpha = Math.min(1, (z - 0.35) * 3);
210
+ ctx.fillStyle = o.labelColor;
211
+ ctx.fillText(m.label, x + 8, y + 4);
212
+ }
213
+ });
214
+ ctx.globalAlpha = 1;
215
+ }
216
+
217
+ const loop = (now) => {
218
+ const dt = Math.min(0.05, (now - last) / 1000);
219
+ last = now;
220
+ time += dt;
221
+ if (!drag) {
222
+ velocity *= 0.94;
223
+ yaw += velocity * dt * 60 + (reduced ? 0 : opts.current.autoRotateSpeed * dt) * (Math.abs(velocity) < 0.05 ? 1 : 0.3);
224
+ }
225
+ draw();
226
+ raf = requestAnimationFrame(loop);
227
+ };
228
+ const start = () => {
229
+ if (raf || !visible) return;
230
+ last = performance.now();
231
+ raf = requestAnimationFrame(loop);
232
+ };
233
+ const stop = () => {
234
+ cancelAnimationFrame(raf);
235
+ raf = 0;
236
+ };
237
+
238
+ const onDown = (e) => {
239
+ drag = { x: e.clientX, yaw, lastX: e.clientX, lastT: performance.now() };
240
+ canvas.setPointerCapture?.(e.pointerId);
241
+ };
242
+ const onMove = (e) => {
243
+ if (!drag) return;
244
+ const degPerPx = 180 / (size * 0.8);
245
+ // Drag right → the surface under the pointer moves right.
246
+ yaw = drag.yaw + (e.clientX - drag.x) * degPerPx;
247
+ const now = performance.now();
248
+ velocity = ((e.clientX - drag.lastX) * degPerPx) / Math.max(1, (now - drag.lastT) / 16.7);
249
+ drag.lastX = e.clientX;
250
+ drag.lastT = now;
251
+ };
252
+ const onUp = () => {
253
+ drag = null;
254
+ };
255
+
256
+ canvas.addEventListener("pointerdown", onDown);
257
+ canvas.addEventListener("pointermove", onMove);
258
+ canvas.addEventListener("pointerup", onUp);
259
+ canvas.addEventListener("pointercancel", onUp);
260
+ const ro = new ResizeObserver(resize);
261
+ ro.observe(wrap);
262
+ const io = new IntersectionObserver(([entry]) => {
263
+ visible = entry.isIntersecting;
264
+ if (visible) start();
265
+ else stop();
266
+ });
267
+ io.observe(wrap);
268
+ resize();
269
+ start();
270
+
271
+ return () => {
272
+ stop();
273
+ ro.disconnect();
274
+ io.disconnect();
275
+ canvas.removeEventListener("pointerdown", onDown);
276
+ canvas.removeEventListener("pointermove", onMove);
277
+ canvas.removeEventListener("pointerup", onUp);
278
+ canvas.removeEventListener("pointercancel", onUp);
279
+ };
280
+ }, []);
281
+
282
+ return (
283
+ <div ref={wrapRef} className={`relative aspect-square w-full ${className}`} style={{ maxWidth: `${maxSize}rem` }}>
284
+ <canvas ref={canvasRef} role="img" aria-label="Interactive globe" className="h-full w-full cursor-grab touch-pan-y active:cursor-grabbing" />
285
+ </div>
286
+ );
287
+ };
288
+
289
+ export default DotGlobe;