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,67 @@
1
+ import React, { useEffect, useRef } from "react";
2
+
3
+ /*
4
+ * Magnet Lines: a grid of short lines that all swing to point at the
5
+ * pointer, like iron filings around a magnet. Lines near the pointer glow
6
+ * and stretch a little. When the pointer leaves they drift back to a slow
7
+ * wave.
8
+ */
9
+
10
+ const MagnetLines = ({ rows = 9, columns = 9, size = 22, lineColor = "currentColor", activeColor = "#a3e635", lineWidth = 0.22, lineHeight = 1.6, baseAngle = -10, className = "" }) => {
11
+ const wrap = useRef(null);
12
+ const lines = useRef([]);
13
+ const pointer = useRef(null);
14
+
15
+ useEffect(() => {
16
+ let raf = 0;
17
+ const angles = lines.current.map(() => baseAngle);
18
+ const tick = (now) => {
19
+ const p = pointer.current;
20
+ const t = now / 1000;
21
+ lines.current.forEach((el, i) => {
22
+ if (!el) return;
23
+ const r = el.getBoundingClientRect();
24
+ const cx = r.left + r.width / 2;
25
+ const cy = r.top + r.height / 2;
26
+ let target;
27
+ let near = 0;
28
+ if (p) {
29
+ const dx = p.x - cx;
30
+ const dy = p.y - cy;
31
+ target = (Math.atan2(dy, dx) * 180) / Math.PI + 90;
32
+ near = Math.max(0, 1 - Math.hypot(dx, dy) / 220);
33
+ } else {
34
+ const col = i % columns;
35
+ const row = Math.floor(i / columns);
36
+ target = baseAngle + Math.sin(t * 1.2 + col * 0.5 + row * 0.3) * 25;
37
+ }
38
+ // Turn the short way round.
39
+ const diff = ((target - angles[i] + 540) % 360) - 180;
40
+ angles[i] += diff * 0.18;
41
+ el.style.transform = `rotate(${angles[i]}deg) scaleY(${1 + near * 0.35})`;
42
+ el.style.background = near > 0.05 ? `color-mix(in srgb, ${activeColor} ${Math.round(near * 100)}%, ${lineColor})` : lineColor;
43
+ el.style.opacity = String(0.35 + 0.65 * Math.max(near, p ? 0.25 : 0.4));
44
+ });
45
+ raf = requestAnimationFrame(tick);
46
+ };
47
+ raf = requestAnimationFrame(tick);
48
+ return () => cancelAnimationFrame(raf);
49
+ }, [baseAngle, columns, lineColor, activeColor]);
50
+
51
+ return (
52
+ <div
53
+ ref={wrap}
54
+ className={`grid touch-none place-items-center ${className}`}
55
+ style={{ gridTemplateColumns: `repeat(${columns}, 1fr)`, width: `min(${size}rem, 90vw)`, aspectRatio: `${columns} / ${rows}` }}
56
+ onPointerMove={(e) => (pointer.current = { x: e.clientX, y: e.clientY })}
57
+ onPointerLeave={() => (pointer.current = null)}
58
+ aria-hidden="true"
59
+ >
60
+ {Array.from({ length: rows * columns }, (_, i) => (
61
+ <span key={i} ref={(el) => (lines.current[i] = el)} className="block rounded-full will-change-transform" style={{ width: `${lineWidth}rem`, height: `${lineHeight}rem`, background: lineColor }} />
62
+ ))}
63
+ </div>
64
+ );
65
+ };
66
+
67
+ export default MagnetLines;
@@ -1,168 +1,15 @@
1
- import React, { useEffect, useRef, useState } from "react";
1
+ import React, { useRef, useState } from "react";
2
2
  import { motion, useMotionValue, useReducedMotion, useSpring } from "framer-motion";
3
3
 
4
4
  /*
5
5
  * Magnetic Cursor: a dot + ring cursor for one area of the page. The ring
6
6
  * wraps around links and buttons it hovers, grows into a labelled bubble over
7
7
  * anything with data-cursor-text, and pulls data-magnetic elements toward the
8
- * pointer. An optional tail follows it: comet, dots, sparkles or ghost rings.
9
- * Touch devices keep their normal behaviour.
8
+ * pointer. Touch devices keep their normal behaviour.
10
9
  */
11
10
 
12
11
  const TARGETS = "a, button, [role='button'], [data-cursor], [data-cursor-text], [data-magnetic]";
13
12
 
14
- const hexToRgb = (hex) => {
15
- let h = String(hex).replace("#", "");
16
- if (h.length === 3) h = h.split("").map((c) => c + c).join("");
17
- const n = parseInt(h, 16) || 0;
18
- return `${(n >> 16) & 255},${(n >> 8) & 255},${n & 255}`;
19
- };
20
-
21
- /*
22
- * The tail lives on one canvas. `feed` gets every pointer position; the loop
23
- * only runs while something is still visible, then sleeps.
24
- */
25
- function useTrail(canvasRef, style, color, enabled) {
26
- const state = useRef({ points: [], dots: [], parts: [], rings: [], last: null, raf: 0, lastSpawn: 0 });
27
-
28
- useEffect(() => {
29
- const canvas = canvasRef.current;
30
- if (!canvas || !enabled || style === "none") return undefined;
31
- const ctx = canvas.getContext("2d");
32
- const s = state.current;
33
- const rgb = hexToRgb(color);
34
- const resize = () => {
35
- const r = canvas.getBoundingClientRect();
36
- const dpr = Math.min(window.devicePixelRatio || 1, 2);
37
- canvas.width = Math.round(r.width * dpr);
38
- canvas.height = Math.round(r.height * dpr);
39
- ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
40
- };
41
- const ro = new ResizeObserver(resize);
42
- ro.observe(canvas);
43
- resize();
44
-
45
- const frame = () => {
46
- const now = performance.now();
47
- const { width, height } = canvas.getBoundingClientRect();
48
- ctx.clearRect(0, 0, width, height);
49
- let alive = false;
50
-
51
- if (style === "comet") {
52
- s.points = s.points.filter((p) => now - p.t < 320);
53
- const n = s.points.length;
54
- if (n > 1) {
55
- alive = true;
56
- ctx.lineCap = "round";
57
- ctx.shadowColor = `rgba(${rgb},0.8)`;
58
- ctx.shadowBlur = 12;
59
- for (let i = 1; i < n; i++) {
60
- const k = i / n;
61
- ctx.strokeStyle = `rgba(${rgb},${k * 0.9})`;
62
- ctx.lineWidth = 1 + k * 7;
63
- ctx.beginPath();
64
- ctx.moveTo(s.points[i - 1].x, s.points[i - 1].y);
65
- ctx.lineTo(s.points[i].x, s.points[i].y);
66
- ctx.stroke();
67
- }
68
- ctx.shadowBlur = 0;
69
- }
70
- }
71
-
72
- if (style === "dots" && s.last) {
73
- // Each dot eases toward the one ahead of it.
74
- if (!s.dots.length) s.dots = Array.from({ length: 14 }, () => ({ x: s.last.x, y: s.last.y }));
75
- let tx = s.last.x;
76
- let ty = s.last.y;
77
- s.dots.forEach((d, i) => {
78
- d.x += (tx - d.x) * 0.38;
79
- d.y += (ty - d.y) * 0.38;
80
- tx = d.x;
81
- ty = d.y;
82
- const k = 1 - i / s.dots.length;
83
- ctx.fillStyle = `rgba(${rgb},${k * 0.8})`;
84
- ctx.beginPath();
85
- ctx.arc(d.x, d.y, 1.5 + k * 4, 0, Math.PI * 2);
86
- ctx.fill();
87
- });
88
- const tail = s.dots[s.dots.length - 1];
89
- alive = Math.hypot(tail.x - s.last.x, tail.y - s.last.y) > 0.5;
90
- }
91
-
92
- if (style === "sparkles") {
93
- s.parts = s.parts.filter((p) => now - p.born < p.life);
94
- s.parts.forEach((p) => {
95
- const k = 1 - (now - p.born) / p.life;
96
- p.x += p.vx;
97
- p.y += p.vy;
98
- p.vy += 0.05;
99
- p.vx *= 0.97;
100
- const size = p.size * k;
101
- ctx.strokeStyle = p.white ? `rgba(255,255,255,${k})` : `rgba(${rgb},${k})`;
102
- ctx.lineWidth = 1.4;
103
- ctx.beginPath();
104
- ctx.moveTo(p.x - size, p.y);
105
- ctx.lineTo(p.x + size, p.y);
106
- ctx.moveTo(p.x, p.y - size);
107
- ctx.lineTo(p.x, p.y + size);
108
- ctx.stroke();
109
- });
110
- alive = s.parts.length > 0;
111
- }
112
-
113
- if (style === "ghost") {
114
- s.rings = s.rings.filter((r) => now - r.born < 650);
115
- s.rings.forEach((r) => {
116
- const k = (now - r.born) / 650;
117
- ctx.strokeStyle = `rgba(${rgb},${(1 - k) * 0.7})`;
118
- ctx.lineWidth = 2 * (1 - k) + 0.5;
119
- ctx.beginPath();
120
- ctx.arc(r.x, r.y, 6 + k * 22, 0, Math.PI * 2);
121
- ctx.stroke();
122
- });
123
- alive = s.rings.length > 0;
124
- }
125
-
126
- s.raf = alive ? requestAnimationFrame(frame) : 0;
127
- };
128
-
129
- s.wake = () => {
130
- if (!s.raf) s.raf = requestAnimationFrame(frame);
131
- };
132
- return () => {
133
- cancelAnimationFrame(s.raf);
134
- s.raf = 0;
135
- s.wake = null;
136
- ro.disconnect();
137
- ctx.clearRect(0, 0, canvas.width, canvas.height);
138
- s.points = [];
139
- s.dots = [];
140
- s.parts = [];
141
- s.rings = [];
142
- };
143
- }, [canvasRef, style, color, enabled]);
144
-
145
- // Called on every pointer move with coordinates inside the area.
146
- return (x, y) => {
147
- const s = state.current;
148
- const now = performance.now();
149
- const prev = s.last;
150
- s.last = { x, y };
151
- if (style === "comet") s.points.push({ x, y, t: now });
152
- if (style === "sparkles" && prev) {
153
- const speed = Math.hypot(x - prev.x, y - prev.y);
154
- for (let i = 0; i < Math.min(4, Math.ceil(speed / 8)); i++) {
155
- 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, born: now, white: Math.random() < 0.3 });
156
- }
157
- }
158
- if (style === "ghost" && now - s.lastSpawn > 70) {
159
- s.lastSpawn = now;
160
- s.rings.push({ x, y, born: now });
161
- }
162
- s.wake?.();
163
- };
164
- }
165
-
166
13
  const MagneticCursor = ({
167
14
  children,
168
15
  color = "#a3e635",
@@ -170,14 +17,11 @@ const MagneticCursor = ({
170
17
  padding = 6,
171
18
  magnetStrength = 0.35,
172
19
  blend = false,
173
- trail = "none",
174
20
  className = "",
175
21
  }) => {
176
22
  const ref = useRef(null);
177
- const trailRef = useRef(null);
178
23
  const magnet = useRef(null);
179
24
  const reduced = useReducedMotion();
180
- const feedTrail = useTrail(trailRef, trail, color, !reduced);
181
25
  const [visible, setVisible] = useState(false);
182
26
  const [label, setLabel] = useState("");
183
27
  const [pressed, setPressed] = useState(false);
@@ -208,7 +52,6 @@ const MagneticCursor = ({
208
52
  dotX.set(px);
209
53
  dotY.set(py);
210
54
  setVisible(true);
211
- feedTrail(px, py);
212
55
 
213
56
  const target = e.target.closest?.(TARGETS);
214
57
  const inside = target && ref.current.contains(target) && target !== ref.current;
@@ -272,7 +115,6 @@ const MagneticCursor = ({
272
115
  className={`relative ${visible ? "cursor-none [&_*]:cursor-none" : ""} ${className}`}
273
116
  >
274
117
  {children}
275
- {trail !== "none" && <canvas ref={trailRef} aria-hidden="true" className="pointer-events-none absolute inset-0 z-40 h-full w-full" />}
276
118
  <motion.div
277
119
  aria-hidden="true"
278
120
  className="pointer-events-none absolute left-0 top-0 z-50 flex items-center justify-center border-2 text-[0.72rem] font-semibold uppercase tracking-[0.14em]"
@@ -0,0 +1,75 @@
1
+ import React, { useEffect, useMemo, useRef, useState } from "react";
2
+
3
+ /*
4
+ * Pixel Transition: hover (or tap) the card and it dissolves into a grid of
5
+ * pixels that pop in at random, the content swaps underneath, then the
6
+ * pixels clear away to reveal the second side.
7
+ */
8
+
9
+ const IK = "https://ik.imagekit.io/ny6yinyut/apexui/assets";
10
+
11
+ const DefaultFirst = () => <img src={`${IK}/black-panther.png?tr=f-auto,q-80,w-600`} alt="Black Panther" draggable="false" className="h-full w-full bg-neutral-900 object-cover" />;
12
+ const DefaultSecond = () => (
13
+ <div className="grid h-full w-full place-items-center bg-neutral-950 p-6 text-center text-white">
14
+ <div>
15
+ <p className="text-[0.7rem] font-semibold uppercase tracking-[0.3em] text-white/50">Wakanda</p>
16
+ <p className="mt-2 text-3xl font-black">Black Panther</p>
17
+ <p className="mt-2 text-sm text-white/60">King T&apos;Challa, protector of his people.</p>
18
+ </div>
19
+ </div>
20
+ );
21
+
22
+ const shuffle = (arr) => {
23
+ const a = [...arr];
24
+ for (let i = a.length - 1; i > 0; i--) {
25
+ const j = Math.floor(Math.random() * (i + 1));
26
+ [a[i], a[j]] = [a[j], a[i]];
27
+ }
28
+ return a;
29
+ };
30
+
31
+ const PixelTransition = ({ first = <DefaultFirst />, second = <DefaultSecond />, gridSize = 10, pixelColor = "#a3e635", duration = 0.45, aspectRatio = "4 / 5", width = 16, className = "" }) => {
32
+ const [showSecond, setShowSecond] = useState(false);
33
+ const pixels = useRef([]);
34
+ const timers = useRef([]);
35
+ const target = useRef(false);
36
+ const cells = useMemo(() => Array.from({ length: gridSize * gridSize }, (_, i) => i), [gridSize]);
37
+
38
+ useEffect(() => () => timers.current.forEach(clearTimeout), []);
39
+
40
+ const run = (toSecond) => {
41
+ if (target.current === toSecond) return;
42
+ target.current = toSecond;
43
+ timers.current.forEach(clearTimeout);
44
+ timers.current = [];
45
+ const half = (duration * 1000) / 2;
46
+ const order = shuffle(cells);
47
+ const per = half / order.length;
48
+ order.forEach((c, k) => timers.current.push(setTimeout(() => pixels.current[c] && (pixels.current[c].style.opacity = "1"), k * per)));
49
+ timers.current.push(
50
+ setTimeout(() => {
51
+ setShowSecond(toSecond);
52
+ shuffle(cells).forEach((c, k) => timers.current.push(setTimeout(() => pixels.current[c] && (pixels.current[c].style.opacity = "0"), k * per)));
53
+ }, half + 30),
54
+ );
55
+ };
56
+
57
+ return (
58
+ <div
59
+ className={`relative cursor-pointer overflow-hidden rounded-2xl ring-1 ring-white/10 ${className}`}
60
+ style={{ aspectRatio, width: `min(${width}rem, 80vw)` }}
61
+ onPointerEnter={(e) => e.pointerType === "mouse" && run(true)}
62
+ onPointerLeave={(e) => e.pointerType === "mouse" && run(false)}
63
+ onClick={() => run(!target.current)}
64
+ >
65
+ <div className="absolute inset-0">{showSecond ? second : first}</div>
66
+ <div className="pointer-events-none absolute inset-0 grid" style={{ gridTemplateColumns: `repeat(${gridSize}, 1fr)`, gridTemplateRows: `repeat(${gridSize}, 1fr)` }} aria-hidden="true">
67
+ {cells.map((c) => (
68
+ <span key={c} ref={(el) => (pixels.current[c] = el)} style={{ background: pixelColor, opacity: 0 }} />
69
+ ))}
70
+ </div>
71
+ </div>
72
+ );
73
+ };
74
+
75
+ export default PixelTransition;
@@ -132,7 +132,7 @@ const PointerFollower = ({
132
132
  >
133
133
  <div className="px-4 py-3 rounded-full text-[11px] overflow-hidden font-semibold shadow-[0_2px_8px_rgba(0,0,0,0.25)] border border-black/10" style={{ backgroundColor: badgeColor }}>
134
134
  <span className="text-[12px] font-medium tracking-wide whitespace-nowrap drop-shadow" style={{ color: badgeTextColor }}>
135
- {person.name} — {person.job}
135
+ {person.name} none {person.job}
136
136
  </span>
137
137
  </div>
138
138
  </div>
@@ -0,0 +1,115 @@
1
+ import React, { useId, useState } from "react";
2
+ import { AnimatePresence, motion } from "framer-motion";
3
+ import { Check, Sparkles } from "lucide-react";
4
+
5
+ /*
6
+ * Pricing Cards: plans side by side with a monthly / yearly switch. Prices
7
+ * slide to their new value, the featured plan glows, and every card lifts
8
+ * on hover. Wraps to one column on small screens.
9
+ */
10
+
11
+ const DEFAULT_PLANS = [
12
+ { name: "Starter", monthly: 0, yearly: 0, description: "For side projects and trying things out.", features: ["10 components", "Community support", "MIT licence"], cta: "Start free" },
13
+ { name: "Pro", monthly: 19, yearly: 15, description: "For freelancers shipping client work.", features: ["All 60+ components", "Figma kit", "Priority support", "Lifetime updates"], cta: "Go Pro", featured: true },
14
+ { name: "Team", monthly: 49, yearly: 39, description: "For product teams that move fast.", features: ["Everything in Pro", "5 seats", "Private Slack", "Custom requests"], cta: "Contact sales" },
15
+ ];
16
+
17
+ function Price({ value, currency }) {
18
+ return (
19
+ <span className="relative inline-flex h-[1.1em] overflow-hidden align-bottom tabular-nums">
20
+ <AnimatePresence mode="popLayout" initial={false}>
21
+ <motion.span
22
+ key={value}
23
+ initial={{ y: "100%", opacity: 0 }}
24
+ animate={{ y: 0, opacity: 1 }}
25
+ exit={{ y: "-100%", opacity: 0 }}
26
+ transition={{ type: "spring", stiffness: 380, damping: 30 }}
27
+ >
28
+ {currency}
29
+ {value}
30
+ </motion.span>
31
+ </AnimatePresence>
32
+ </span>
33
+ );
34
+ }
35
+
36
+ const PricingCards = ({ plans = DEFAULT_PLANS, currency = "$", yearlyNote = "Save 20%", accentColor = "#a3e635", theme = "dark", onSelect, className = "" }) => {
37
+ const id = useId();
38
+ const [yearly, setYearly] = useState(false);
39
+ const dark = theme !== "light";
40
+
41
+ return (
42
+ <div className={`w-full ${dark ? "text-white" : "text-neutral-900"} ${className}`}>
43
+ <div className="mb-8 flex items-center justify-center gap-3 text-sm font-medium">
44
+ <div className={`relative flex rounded-full p-1 ${dark ? "bg-white/[0.06]" : "bg-black/[0.05]"}`} role="radiogroup" aria-label="Billing period">
45
+ {[false, true].map((y) => (
46
+ <button
47
+ key={String(y)}
48
+ type="button"
49
+ role="radio"
50
+ aria-checked={yearly === y}
51
+ onClick={() => setYearly(y)}
52
+ className={`relative rounded-full px-4 py-1.5 transition-colors ${yearly === y ? "text-neutral-950" : dark ? "text-white/60 hover:text-white" : "text-neutral-500 hover:text-neutral-900"}`}
53
+ >
54
+ {yearly === y && <motion.span layoutId={`${id}-period`} className="absolute inset-0 rounded-full" style={{ background: accentColor }} transition={{ type: "spring", stiffness: 480, damping: 36 }} />}
55
+ <span className="relative">{y ? "Yearly" : "Monthly"}</span>
56
+ </button>
57
+ ))}
58
+ </div>
59
+ <span className="rounded-full px-2 py-0.5 text-[0.72rem] font-semibold" style={{ background: `color-mix(in srgb, ${accentColor} 18%, transparent)`, color: dark ? accentColor : `color-mix(in srgb, ${accentColor} 55%, #000)` }}>
60
+ {yearlyNote}
61
+ </span>
62
+ </div>
63
+
64
+ <div className="grid items-stretch gap-4 [grid-template-columns:repeat(auto-fit,minmax(min(100%,12.5rem),1fr))]">
65
+ {plans.map((plan) => {
66
+ const price = yearly ? plan.yearly : plan.monthly;
67
+ return (
68
+ <motion.article
69
+ key={plan.name}
70
+ whileHover={{ y: -6 }}
71
+ transition={{ type: "spring", stiffness: 300, damping: 24 }}
72
+ className={`relative flex flex-col rounded-3xl border p-6 ${dark ? "bg-neutral-950" : "bg-white"} ${plan.featured ? "" : dark ? "border-white/10" : "border-black/10"}`}
73
+ style={plan.featured ? { borderColor: accentColor, boxShadow: `0 0 0 1px ${accentColor}, 0 24px 60px -24px ${accentColor}88` } : undefined}
74
+ >
75
+ {plan.featured && (
76
+ <span className="absolute -top-3 left-6 inline-flex items-center gap-1 rounded-full px-2.5 py-1 text-[0.7rem] font-bold text-neutral-950" style={{ background: accentColor }}>
77
+ <Sparkles className="h-3 w-3" /> Most popular
78
+ </span>
79
+ )}
80
+ <h3 className="text-lg font-semibold">{plan.name}</h3>
81
+ <p className={`mt-1 text-[0.85rem] ${dark ? "text-white/55" : "text-neutral-500"}`}>{plan.description}</p>
82
+ <p className="mt-5 flex items-end gap-1.5">
83
+ <span className="text-4xl font-bold tracking-tight">
84
+ <Price value={price} currency={currency} />
85
+ </span>
86
+ <span className={`mb-1 text-sm ${dark ? "text-white/45" : "text-neutral-400"}`}>/ month</span>
87
+ </p>
88
+ <p className={`h-5 text-[0.75rem] ${dark ? "text-white/40" : "text-neutral-400"}`}>{yearly && price > 0 ? `${currency}${price * 12} billed yearly` : ""}</p>
89
+ <ul className="mt-5 flex-1 space-y-2.5 text-[0.88rem]">
90
+ {plan.features.map((f) => (
91
+ <li key={f} className="flex items-start gap-2.5">
92
+ <span className="mt-0.5 grid h-4 w-4 shrink-0 place-items-center rounded-full" style={{ background: `color-mix(in srgb, ${accentColor} 22%, transparent)` }}>
93
+ <Check className="h-3 w-3" style={{ color: dark ? accentColor : `color-mix(in srgb, ${accentColor} 50%, #000)` }} />
94
+ </span>
95
+ <span className={dark ? "text-white/80" : "text-neutral-700"}>{f}</span>
96
+ </li>
97
+ ))}
98
+ </ul>
99
+ <button
100
+ type="button"
101
+ onClick={() => onSelect?.(plan, yearly ? "yearly" : "monthly")}
102
+ className={`mt-6 h-11 rounded-full text-sm font-semibold transition-[transform,filter] duration-200 hover:brightness-105 active:scale-[0.97] ${plan.featured ? "text-neutral-950" : dark ? "bg-white/[0.08] text-white hover:bg-white/[0.12]" : "bg-black/[0.05] text-neutral-900 hover:bg-black/[0.08]"}`}
103
+ style={plan.featured ? { background: accentColor } : undefined}
104
+ >
105
+ {plan.cta}
106
+ </button>
107
+ </motion.article>
108
+ );
109
+ })}
110
+ </div>
111
+ </div>
112
+ );
113
+ };
114
+
115
+ export default PricingCards;
@@ -0,0 +1,66 @@
1
+ import React, { useEffect, useRef } from "react";
2
+
3
+ /*
4
+ * Proximity Text: letters swell, lift and light up as the pointer gets
5
+ * close, falling off smoothly with distance — like a magnifier sliding
6
+ * over the line. Works on touch too (drag a finger across).
7
+ */
8
+
9
+ const ProximityText = ({ text = "Hover over these letters", radius = 110, maxScale = 1.6, lift = 0.18, color = "currentColor", activeColor = "#a3e635", className = "" }) => {
10
+ const wrap = useRef(null);
11
+ const letters = useRef([]);
12
+ const pointer = useRef(null);
13
+
14
+ useEffect(() => {
15
+ let raf = 0;
16
+ const amounts = letters.current.map(() => 0);
17
+ const tick = () => {
18
+ const p = pointer.current;
19
+ letters.current.forEach((el, i) => {
20
+ if (!el) return;
21
+ let target = 0;
22
+ if (p) {
23
+ const r = el.getBoundingClientRect();
24
+ const d = Math.hypot(p.x - (r.left + r.width / 2), p.y - (r.top + r.height / 2));
25
+ target = Math.max(0, 1 - d / radius);
26
+ target = target * target * (3 - 2 * target); // smoothstep
27
+ }
28
+ amounts[i] += (target - amounts[i]) * 0.2;
29
+ const a = amounts[i];
30
+ el.style.transform = `translateY(${-a * lift}em) scale(${1 + a * (maxScale - 1)})`;
31
+ el.style.color = a > 0.02 ? `color-mix(in srgb, ${activeColor} ${Math.round(a * 100)}%, ${color})` : "";
32
+ });
33
+ raf = requestAnimationFrame(tick);
34
+ };
35
+ raf = requestAnimationFrame(tick);
36
+ return () => cancelAnimationFrame(raf);
37
+ }, [radius, maxScale, lift, color, activeColor]);
38
+
39
+ const words = text.split(" ");
40
+ let n = 0;
41
+ return (
42
+ <span
43
+ ref={wrap}
44
+ className={`inline-flex touch-none flex-wrap justify-center gap-x-[0.3em] font-bold ${className}`}
45
+ style={{ color }}
46
+ onPointerMove={(e) => (pointer.current = { x: e.clientX, y: e.clientY })}
47
+ onPointerLeave={() => (pointer.current = null)}
48
+ aria-label={text}
49
+ >
50
+ {words.map((w, wi) => (
51
+ <span key={wi} className="inline-flex" aria-hidden="true">
52
+ {Array.from(w).map((c) => {
53
+ const k = n++;
54
+ return (
55
+ <span key={k} ref={(el) => (letters.current[k] = el)} className="inline-block origin-bottom will-change-transform">
56
+ {c}
57
+ </span>
58
+ );
59
+ })}
60
+ </span>
61
+ ))}
62
+ </span>
63
+ );
64
+ };
65
+
66
+ export default ProximityText;
@@ -0,0 +1,50 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { AnimatePresence, motion } from "framer-motion";
3
+
4
+ /*
5
+ * Rotating Text: a phrase whose last word swaps on a timer. Each letter of
6
+ * the new word springs up from below while the old one slides out, and the
7
+ * highlight pill resizes smoothly to fit.
8
+ */
9
+
10
+ const RotatingText = ({ prefix = "Build", words = ["faster", "smarter", "together", "with ApexUI"], interval = 2200, stagger = 0.025, from = "first", accentColor = "#a3e635", textColor = "#0a0a0a", className = "" }) => {
11
+ const [i, setI] = useState(0);
12
+ useEffect(() => {
13
+ const id = setInterval(() => setI((n) => (n + 1) % words.length), interval);
14
+ return () => clearInterval(id);
15
+ }, [words.length, interval]);
16
+
17
+ const word = words[i];
18
+ const chars = Array.from(word);
19
+ const delayFor = (k) => {
20
+ const n = chars.length - 1;
21
+ const order = from === "last" ? n - k : from === "center" ? Math.abs(k - n / 2) : k;
22
+ return order * stagger;
23
+ };
24
+
25
+ return (
26
+ <span className={`inline-flex flex-wrap items-center gap-x-[0.3em] font-bold ${className}`}>
27
+ {prefix && <span>{prefix}</span>}
28
+ <motion.span layout transition={{ type: "spring", stiffness: 300, damping: 30 }} className="relative inline-flex overflow-hidden rounded-[0.35em] px-[0.35em] py-[0.08em]" style={{ background: accentColor, color: textColor }}>
29
+ <AnimatePresence mode="popLayout" initial={false}>
30
+ <motion.span key={word} className="inline-flex whitespace-pre" aria-label={word}>
31
+ {chars.map((c, k) => (
32
+ <motion.span
33
+ key={k}
34
+ aria-hidden="true"
35
+ className="inline-block"
36
+ initial={{ y: "110%", opacity: 0 }}
37
+ animate={{ y: 0, opacity: 1, transition: { type: "spring", stiffness: 420, damping: 30, delay: delayFor(k) } }}
38
+ exit={{ y: "-120%", opacity: 0, transition: { duration: 0.22, ease: "easeIn", delay: delayFor(k) * 0.6 } }}
39
+ >
40
+ {c}
41
+ </motion.span>
42
+ ))}
43
+ </motion.span>
44
+ </AnimatePresence>
45
+ </motion.span>
46
+ </span>
47
+ );
48
+ };
49
+
50
+ export default RotatingText;
@@ -0,0 +1,36 @@
1
+ import React from "react";
2
+ import { motion, useReducedMotion } from "framer-motion";
3
+
4
+ /*
5
+ * Shiny Text: a bright band of light sweeps across the text on a loop, like
6
+ * light catching metal. The band starts and ends fully outside the text, so
7
+ * the loop is seamless — it never pauses on the last letters or jumps back.
8
+ */
9
+
10
+ const ShinyText = ({ text = "Shipping soon, join the waitlist", as = "span", speed = 2.8, color = "#71717a", shineColor = "#ffffff", spread = 10, pause = 0.6, disabled = false, className = "" }) => {
11
+ const reduced = useReducedMotion();
12
+ const Tag = motion[as] ?? motion.span;
13
+ const still = disabled || reduced;
14
+ // The gradient is 3× the text width with the band in its middle. Moving the
15
+ // background from 100% to 0% carries the band from fully left of the text
16
+ // to fully right of it, so both ends of the loop look identical.
17
+ const band = Math.min(16, Math.max(4, spread));
18
+ return (
19
+ <Tag
20
+ className={`inline-block bg-clip-text text-transparent ${className}`}
21
+ style={{
22
+ backgroundImage: `linear-gradient(100deg, ${color} ${50 - band}%, ${shineColor} 50%, ${color} ${50 + band}%)`,
23
+ backgroundSize: "300% 100%",
24
+ backgroundRepeat: "no-repeat",
25
+ WebkitBackgroundClip: "text",
26
+ backgroundPosition: "100% 0",
27
+ }}
28
+ animate={still ? { backgroundPosition: "100% 0" } : { backgroundPosition: ["100% 0", "0% 0"] }}
29
+ transition={still ? undefined : { duration: speed, ease: "linear", repeat: Infinity, repeatDelay: pause }}
30
+ >
31
+ {text}
32
+ </Tag>
33
+ );
34
+ };
35
+
36
+ export default ShinyText;
@@ -0,0 +1,33 @@
1
+ import React from "react";
2
+ import { motion, useReducedMotion } from "framer-motion";
3
+
4
+ /*
5
+ * Star Border: two soft points of light travel along the top and bottom
6
+ * edges of a button in opposite directions, like stars sliding around its
7
+ * rim. Renders a <button> by default; pass `as="a"` for a link.
8
+ */
9
+
10
+ const StarBorder = ({ as = "button", color = "#a3e635", speed = 5, thickness = 1.5, theme = "dark", children = "Get started", className = "", ...rest }) => {
11
+ const reduced = useReducedMotion();
12
+ const Tag = as;
13
+ const dark = theme !== "light";
14
+ const star = (dir) => ({
15
+ background: `radial-gradient(circle, ${color}, transparent 10%)`,
16
+ animate: reduced ? undefined : { x: dir > 0 ? ["-35%", "35%"] : ["35%", "-35%"], opacity: [0, 1, 1, 0] },
17
+ transition: { duration: speed, ease: "linear", repeat: Infinity, times: [0, 0.15, 0.85, 1] },
18
+ });
19
+ const top = star(-1);
20
+ const bottom = star(1);
21
+
22
+ return (
23
+ <Tag className={`group relative inline-block overflow-hidden rounded-[1.1rem] ${className}`} style={{ padding: `${thickness}px 0` }} {...(as === "button" ? { type: "button" } : {})} {...rest}>
24
+ <motion.span aria-hidden="true" className="pointer-events-none absolute -bottom-[11px] left-[-100%] h-1/2 w-[300%] opacity-70" style={{ background: bottom.background }} animate={bottom.animate} transition={bottom.transition} />
25
+ <motion.span aria-hidden="true" className="pointer-events-none absolute -top-[10px] left-[-100%] h-1/2 w-[300%] opacity-70" style={{ background: top.background }} animate={top.animate} transition={top.transition} />
26
+ <span className={`relative z-[1] block rounded-[1.1rem] border px-7 py-3.5 text-center text-[0.95rem] font-semibold transition-colors ${dark ? "border-white/10 bg-gradient-to-b from-neutral-900 to-black text-white group-hover:border-white/20" : "border-black/10 bg-gradient-to-b from-white to-neutral-100 text-neutral-900 group-hover:border-black/20"}`}>
27
+ {children}
28
+ </span>
29
+ </Tag>
30
+ );
31
+ };
32
+
33
+ export default StarBorder;