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,168 @@
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
+ // Block text selection / native drag, which cancel the drag midway.
109
+ e.preventDefault();
110
+ e.currentTarget.setPointerCapture(e.pointerId);
111
+ rotY.stop();
112
+ rotX.stop();
113
+ 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 };
114
+ }}
115
+ onPointerMove={(e) => {
116
+ const d = drag.current;
117
+ if (!d) return;
118
+ rotY.set(d.sx + (e.clientX - d.x) * 0.5);
119
+ rotX.set(Math.max(-70, Math.min(70, d.sy - (e.clientY - d.y) * 0.4)));
120
+ const now = performance.now();
121
+ const dt = Math.max(1, now - d.t);
122
+ d.vx = ((e.clientX - d.lx) / dt) * 0.5 * 1000;
123
+ d.vy = ((e.clientY - d.ly) / dt) * 0.4 * 1000;
124
+ d.t = now;
125
+ d.lx = e.clientX;
126
+ d.ly = e.clientY;
127
+ }}
128
+ onPointerUp={() => {
129
+ const d = drag.current;
130
+ if (!d) return;
131
+ drag.current = null;
132
+ // Throw with momentum, then settle back to the corner tilt.
133
+ animate(rotY, rotY.get() + d.vx * 0.35, { type: "spring", stiffness: 40, damping: 16 });
134
+ animate(rotX, tilt, { type: "spring", stiffness: 70, damping: 14 });
135
+ }}
136
+ >
137
+ {SIDES.map((s, i) => {
138
+ const f = faces[i % faces.length];
139
+ const transform = s.pitch ? `rotateX(${s.pitch}deg) translateZ(${half})` : `rotateY(${s.yaw}deg) translateZ(${half})`;
140
+ return (
141
+ <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)" }}>
142
+ <img src={f.image} alt={f.title} draggable="false" className="h-full w-full select-none object-cover" />
143
+ <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>
144
+ <span ref={(el) => (shades.current[i] = el)} className="pointer-events-none absolute inset-0 bg-black" style={{ opacity: 0.3 }} />
145
+ </figure>
146
+ );
147
+ })}
148
+ </motion.div>
149
+ </div>
150
+
151
+ <div className="flex items-center gap-4">
152
+ <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">
153
+ <ChevronLeft className="h-5 w-5" />
154
+ </button>
155
+ <div className="flex gap-1.5" aria-hidden="true">
156
+ {[0, 1, 2, 3].map((i) => (
157
+ <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)" }} />
158
+ ))}
159
+ </div>
160
+ <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">
161
+ <ChevronRight className="h-5 w-5" />
162
+ </button>
163
+ </div>
164
+ </div>
165
+ );
166
+ };
167
+
168
+ 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;
@@ -236,6 +236,7 @@ const DotGlobe = ({
236
236
  };
237
237
 
238
238
  const onDown = (e) => {
239
+ e.preventDefault();
239
240
  drag = { x: e.clientX, yaw, lastX: e.clientX, lastT: performance.now() };
240
241
  canvas.setPointerCapture?.(e.pointerId);
241
242
  };
@@ -51,7 +51,7 @@ const DynamicIsland = ({
51
51
  accentColor = "#a3e635",
52
52
  music = { title: "Midnight City", artist: "Apex Sessions", cover: `${IK}/spiderman.png?tr=f-auto,q-80,w-200` },
53
53
  call = { name: "Tony Stark", subtitle: "Mobile", avatar: `${IK}/ironman.png?tr=f-auto,q-80,w-160` },
54
- notification = { app: "Messages", title: "Peter Parker", message: "On my way — saving the city first 🕷️" },
54
+ notification = { app: "Messages", title: "Peter Parker", message: "On my way, saving the city first 🕷️" },
55
55
  timerSeconds = 300,
56
56
  onViewChange,
57
57
  className = "",
@@ -0,0 +1,161 @@
1
+ import React, { useEffect, useRef, useState } from "react";
2
+ import { animate, motion, useMotionValue, useTransform } from "framer-motion";
3
+ import { Volume1, Volume2 } from "lucide-react";
4
+
5
+ /*
6
+ * Elastic Slider: a volume-style slider with a knob. Grab anywhere on the
7
+ * track and drag; pull past either end and the track stretches like rubber
8
+ * (the end icon gets dragged along), then springs back when you let go.
9
+ * The fill follows the pointer every frame — no React render in the loop.
10
+ * Arrow keys, Page Up/Down, Home and End work too.
11
+ */
12
+
13
+ const MAX_OVERFLOW = 56;
14
+ // Diminishing stretch: the further you pull, the less it gives.
15
+ const decay = (v, max) => (2 / (1 + Math.exp(-v / max)) - 1) * max;
16
+
17
+ const ElasticSlider = ({
18
+ defaultValue = 50,
19
+ min = 0,
20
+ max = 100,
21
+ step = 1,
22
+ leftIcon = <Volume1 className="h-5 w-5" />,
23
+ rightIcon = <Volume2 className="h-5 w-5" />,
24
+ accentColor = "#a3e635",
25
+ onChange,
26
+ showValue = true,
27
+ className = "",
28
+ }) => {
29
+ const clamp = (v) => Math.min(max, Math.max(min, v));
30
+ const [value, setValue] = useState(clamp(defaultValue));
31
+ const [dragging, setDragging] = useState(false);
32
+ const [hover, setHover] = useState(false);
33
+ const track = useRef(null);
34
+ const drag = useRef(false);
35
+ // Latest value, readable synchronously from pointer handlers (state lags a render).
36
+ const valueRef = useRef(clamp(defaultValue));
37
+
38
+ const pct = useMotionValue(((clamp(defaultValue) - min) / (max - min)) * 100);
39
+ const overflow = useMotionValue(0); // px past an end, negative = left
40
+ const fillWidth = useTransform(pct, (p) => `${p}%`);
41
+ const knobLeft = useTransform(pct, (p) => `${p}%`);
42
+ const scaleX = useTransform(overflow, (o) => 1 + Math.abs(o) / (track.current?.offsetWidth || 240));
43
+ const originX = useTransform(overflow, (o) => (o < 0 ? 1 : 0));
44
+ const leftX = useTransform(overflow, (o) => (o < 0 ? o : 0));
45
+ const rightX = useTransform(overflow, (o) => (o > 0 ? o : 0));
46
+
47
+ const commit = (raw) => {
48
+ const v = clamp(Math.round(raw / step) * step);
49
+ if (v !== valueRef.current) {
50
+ valueRef.current = v;
51
+ setValue(v);
52
+ onChange?.(v);
53
+ }
54
+ return v;
55
+ };
56
+
57
+ const fromPointer = (clientX) => {
58
+ const r = track.current.getBoundingClientRect();
59
+ const ratio = (clientX - r.left) / r.width;
60
+ pct.set(Math.min(1, Math.max(0, ratio)) * 100);
61
+ commit(min + Math.min(1, Math.max(0, ratio)) * (max - min));
62
+ const past = clientX < r.left ? clientX - r.left : clientX > r.right ? clientX - r.right : 0;
63
+ overflow.set(Math.sign(past) * decay(Math.abs(past), MAX_OVERFLOW));
64
+ };
65
+
66
+ const end = () => {
67
+ if (!drag.current) return;
68
+ drag.current = false;
69
+ setDragging(false);
70
+ animate(overflow, 0, { type: "spring", stiffness: 380, damping: 12 });
71
+ // Snap the fill to the stepped value.
72
+ animate(pct, ((valueRef.current - min) / (max - min)) * 100, { duration: 0.12 });
73
+ };
74
+
75
+ // Keep the fill in sync for keyboard changes.
76
+ useEffect(() => {
77
+ if (!drag.current) animate(pct, ((value - min) / (max - min)) * 100, { type: "spring", stiffness: 500, damping: 40 });
78
+ }, [value, min, max, pct]);
79
+
80
+ const nudge = (delta) => commit(valueRef.current + delta);
81
+ const big = Math.max(step, (max - min) / 10);
82
+ const thick = dragging || hover;
83
+
84
+ return (
85
+ <div className={`flex w-full max-w-sm flex-col items-center gap-3 ${className}`}>
86
+ <div className="flex w-full touch-none select-none items-center gap-4">
87
+ <motion.button type="button" aria-label="Decrease" onClick={() => nudge(-big)} style={{ x: leftX }} className="shrink-0 opacity-70 transition-opacity hover:opacity-100">
88
+ {leftIcon}
89
+ </motion.button>
90
+
91
+ <div
92
+ ref={track}
93
+ role="slider"
94
+ tabIndex={0}
95
+ aria-valuemin={min}
96
+ aria-valuemax={max}
97
+ aria-valuenow={value}
98
+ aria-label="Value"
99
+ className="relative flex h-10 flex-1 cursor-grab items-center rounded-full outline-none focus-visible:ring-2 focus-visible:ring-current/30 active:cursor-grabbing"
100
+ onPointerEnter={() => setHover(true)}
101
+ onPointerLeave={() => setHover(false)}
102
+ onPointerDown={(e) => {
103
+ if (e.button !== 0) return;
104
+ // No text selection / native drag: those fire pointercancel mid-drag.
105
+ e.preventDefault();
106
+ e.currentTarget.focus({ preventScroll: true });
107
+ e.currentTarget.setPointerCapture(e.pointerId);
108
+ // Stop any spring still settling from the last drag.
109
+ pct.stop();
110
+ overflow.stop();
111
+ drag.current = true;
112
+ setDragging(true);
113
+ fromPointer(e.clientX);
114
+ }}
115
+ onPointerMove={(e) => drag.current && fromPointer(e.clientX)}
116
+ onPointerUp={end}
117
+ onPointerCancel={end}
118
+ onLostPointerCapture={end}
119
+ onKeyDown={(e) => {
120
+ const map = { ArrowRight: step, ArrowUp: step, ArrowLeft: -step, ArrowDown: -step, PageUp: big, PageDown: -big };
121
+ if (e.key in map) nudge(map[e.key]);
122
+ else if (e.key === "Home") commit(min);
123
+ else if (e.key === "End") commit(max);
124
+ else return;
125
+ e.preventDefault();
126
+ }}
127
+ >
128
+ <motion.div className="relative w-full rounded-full" style={{ scaleX, originX }} animate={{ height: thick ? 12 : 7 }} transition={{ type: "spring", stiffness: 400, damping: 30 }}>
129
+ <div className="absolute inset-0 overflow-hidden rounded-full" style={{ background: "color-mix(in srgb, currentColor 16%, transparent)" }}>
130
+ <motion.div className="absolute inset-y-0 left-0 rounded-full" style={{ width: fillWidth, background: accentColor }} />
131
+ </div>
132
+ {/* Knob */}
133
+ <motion.span
134
+ className="absolute top-1/2 block rounded-full bg-white shadow-[0_1px_4px_rgba(0,0,0,0.35)] ring-1 ring-black/10"
135
+ style={{ left: knobLeft, x: "-50%", y: "-50%" }}
136
+ animate={{ width: dragging ? 22 : thick ? 18 : 14, height: dragging ? 22 : thick ? 18 : 14 }}
137
+ transition={{ type: "spring", stiffness: 500, damping: 30 }}
138
+ />
139
+ {/* Value bubble while dragging */}
140
+ <motion.span
141
+ className="pointer-events-none absolute bottom-full mb-3 rounded-md px-1.5 py-0.5 font-mono text-[0.7rem] font-semibold text-black"
142
+ style={{ left: knobLeft, x: "-50%", background: accentColor }}
143
+ initial={false}
144
+ animate={{ opacity: dragging ? 1 : 0, y: dragging ? 0 : 4 }}
145
+ transition={{ duration: 0.15 }}
146
+ >
147
+ {value}
148
+ </motion.span>
149
+ </motion.div>
150
+ </div>
151
+
152
+ <motion.button type="button" aria-label="Increase" onClick={() => nudge(big)} style={{ x: rightX }} className="shrink-0 opacity-70 transition-opacity hover:opacity-100">
153
+ {rightIcon}
154
+ </motion.button>
155
+ </div>
156
+ {showValue && <p className="font-mono text-sm tabular-nums opacity-60">{value}</p>}
157
+ </div>
158
+ );
159
+ };
160
+
161
+ export default ElasticSlider;
@@ -0,0 +1,78 @@
1
+ import React, { useRef } from "react";
2
+ import { motion, useMotionValue, useReducedMotion, useSpring, useTransform } from "framer-motion";
3
+
4
+ /*
5
+ * Extruded Text: chunky 3D letters built from stacked copies of the text,
6
+ * each pushed a little deeper. The whole block tilts toward the pointer so
7
+ * you can see the sides, and the face can carry a gradient.
8
+ */
9
+
10
+ const ExtrudedText = ({
11
+ text = "APEX",
12
+ depth = 14,
13
+ faceColor = "#a3e635",
14
+ sideColor = "#3f6212",
15
+ gradient = "",
16
+ maxTilt = 22,
17
+ fontSize = 7,
18
+ className = "",
19
+ }) => {
20
+ const ref = useRef(null);
21
+ const reduced = useReducedMotion();
22
+ // Rest a little turned so the depth shows before the pointer arrives.
23
+ const REST = { x: -0.55, y: 0.5 };
24
+ const px = useMotionValue(REST.x);
25
+ const py = useMotionValue(REST.y);
26
+ const rotateY = useSpring(useTransform(px, [-1, 1], [-maxTilt, maxTilt]), { stiffness: 140, damping: 16 });
27
+ const rotateX = useSpring(useTransform(py, [-1, 1], [maxTilt * 0.7, -maxTilt * 0.7]), { stiffness: 140, damping: 16 });
28
+
29
+ const onMove = (e) => {
30
+ if (reduced) return;
31
+ const r = ref.current.getBoundingClientRect();
32
+ px.set(((e.clientX - r.left) / r.width) * 2 - 1);
33
+ py.set(((e.clientY - r.top) / r.height) * 2 - 1);
34
+ };
35
+
36
+ const layers = Array.from({ length: depth }, (_, i) => depth - i);
37
+
38
+ return (
39
+ <div
40
+ ref={ref}
41
+ onPointerMove={onMove}
42
+ onPointerLeave={() => {
43
+ px.set(REST.x);
44
+ py.set(REST.y);
45
+ }}
46
+ className={`flex w-full items-center justify-center py-10 [perspective:900px] ${className}`}
47
+ aria-label={text}
48
+ role="img"
49
+ >
50
+ <motion.div className="relative font-black leading-none tracking-tight [transform-style:preserve-3d]" style={{ rotateX, rotateY, fontSize: `clamp(2.5rem, 14vw, ${fontSize}rem)` }}>
51
+ {/* Back layers form the sides, darker the deeper they go. */}
52
+ {layers.map((z) => (
53
+ <span
54
+ key={z}
55
+ aria-hidden="true"
56
+ className="absolute inset-0 select-none"
57
+ style={{ transform: `translateZ(${-z * 3}px)`, color: sideColor, filter: `brightness(${1 - (z / depth) * 0.55})` }}
58
+ >
59
+ {text}
60
+ </span>
61
+ ))}
62
+ <span
63
+ aria-hidden="true"
64
+ className="relative block select-none"
65
+ style={
66
+ gradient
67
+ ? { backgroundImage: gradient, WebkitBackgroundClip: "text", backgroundClip: "text", color: "transparent" }
68
+ : { color: faceColor, textShadow: "0 1px 0 rgba(255,255,255,0.35)" }
69
+ }
70
+ >
71
+ {text}
72
+ </span>
73
+ </motion.div>
74
+ </div>
75
+ );
76
+ };
77
+
78
+ export default ExtrudedText;
@@ -9,7 +9,7 @@ import { Plus } from "lucide-react";
9
9
  */
10
10
 
11
11
  const DEFAULT_ITEMS = [
12
- { q: "Is ApexUI free to use?", a: "Yes. Every component is free and open source — copy it into your project or add it with one CLI command." },
12
+ { q: "Is ApexUI free to use?", a: "Yes. Every component is free and open source, copy it into your project or add it with one CLI command." },
13
13
  { q: "Does it work with Next.js?", a: "Yes. The components are plain React with Tailwind CSS, so they work in Vite, Next.js, Remix and anything else that runs React." },
14
14
  { q: "Can I change the colors?", a: "Every component takes props for its accent colors, and the Tailwind classes are right there in the code if you want to go further." },
15
15
  { q: "Do the animations respect reduced motion?", a: "Yes. Components check the visitor's reduced-motion setting and tone their movement down or switch it off." },