apex-ui-kit 2.2.0 → 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.
- package/dist/ApexUI-Kit/templates/AiChat/AiChat.jsx +171 -0
- package/dist/ApexUI-Kit/templates/AiModelPrompt/AiModelPrompt.jsx +221 -0
- package/dist/ApexUI-Kit/templates/AiModelPrompt/index.js +2 -0
- package/dist/ApexUI-Kit/templates/AiModelPrompt/models.js +15 -0
- package/dist/ApexUI-Kit/templates/AiPromptBox/AiPromptBox.jsx +173 -0
- package/dist/ApexUI-Kit/templates/AiResponse/AiResponse.jsx +172 -0
- package/dist/ApexUI-Kit/templates/AiThinking/AiThinking.jsx +106 -0
- package/dist/ApexUI-Kit/templates/AiVoiceBars/AiVoiceBars.jsx +78 -0
- package/dist/ApexUI-Kit/templates/AiVoiceBlob/AiVoiceBlob.jsx +105 -0
- package/dist/ApexUI-Kit/templates/AiVoiceOrb/AiVoiceOrb.jsx +74 -0
- package/dist/ApexUI-Kit/templates/AiVoiceRing/AiVoiceRing.jsx +120 -0
- package/dist/ApexUI-Kit/templates/AiVoiceWave/AiVoiceWave.jsx +107 -0
- package/dist/ApexUI-Kit/templates/AnimatedSwitch/AnimatedSwitch.jsx +80 -0
- package/dist/ApexUI-Kit/templates/AnimatedTabs/AnimatedTabs.jsx +10 -2
- package/dist/ApexUI-Kit/templates/AuroraBackground/AuroraBackground.jsx +48 -0
- package/dist/ApexUI-Kit/templates/BounceCards/BounceCards.jsx +53 -0
- package/dist/ApexUI-Kit/templates/CardSwap3D/CardSwap3D.jsx +80 -0
- package/dist/ApexUI-Kit/templates/CircularText/CircularText.jsx +49 -0
- package/dist/ApexUI-Kit/templates/ClickSpark/ClickSpark.jsx +72 -0
- package/dist/ApexUI-Kit/templates/Coverflow3D/Coverflow3D.jsx +108 -0
- package/dist/ApexUI-Kit/templates/CubeGallery/CubeGallery.jsx +166 -0
- package/dist/ApexUI-Kit/templates/CursorTrail/CursorTrail.jsx +200 -0
- package/dist/ApexUI-Kit/templates/ElasticSlider/ElasticSlider.jsx +152 -0
- package/dist/ApexUI-Kit/templates/ExtrudedText/ExtrudedText.jsx +78 -0
- package/dist/ApexUI-Kit/templates/FlipBook/FlipBook.jsx +191 -0
- package/dist/ApexUI-Kit/templates/FloatingNavbar/FloatingNavbar.jsx +162 -0
- package/dist/ApexUI-Kit/templates/Folder/Folder.jsx +101 -0
- package/dist/ApexUI-Kit/templates/GlitchText/GlitchText.jsx +56 -0
- package/dist/ApexUI-Kit/templates/GooeyNav/GooeyNav.jsx +310 -0
- package/dist/ApexUI-Kit/templates/HoldToConfirm/HoldToConfirm.jsx +169 -0
- package/dist/ApexUI-Kit/templates/HoldToConfirm/UndoNotice.jsx +58 -0
- package/dist/ApexUI-Kit/templates/HoloCard/HoloCard.jsx +92 -0
- package/dist/ApexUI-Kit/templates/ImageCompare/ImageCompare.jsx +94 -0
- package/dist/ApexUI-Kit/templates/IsoStack/IsoStack.jsx +86 -0
- package/dist/ApexUI-Kit/templates/LoadingScreen/LoadingScreen.jsx +545 -101
- package/dist/ApexUI-Kit/templates/MagnetLines/MagnetLines.jsx +67 -0
- package/dist/ApexUI-Kit/templates/MagneticCursor/MagneticCursor.jsx +2 -160
- package/dist/ApexUI-Kit/templates/PixelTransition/PixelTransition.jsx +75 -0
- package/dist/ApexUI-Kit/templates/PricingCards/PricingCards.jsx +115 -0
- package/dist/ApexUI-Kit/templates/ProximityText/ProximityText.jsx +66 -0
- package/dist/ApexUI-Kit/templates/RotatingText/RotatingText.jsx +50 -0
- package/dist/ApexUI-Kit/templates/ShinyText/ShinyText.jsx +30 -0
- package/dist/ApexUI-Kit/templates/StarBorder/StarBorder.jsx +33 -0
- package/dist/ApexUI-Kit/templates/Stepper/Stepper.jsx +129 -0
- package/dist/ApexUI-Kit/templates/SwipeCards/SwipeCards.jsx +123 -0
- package/dist/ApexUI-Kit/templates/TextRing3D/TextRing3D.jsx +73 -0
- package/dist/ApexUI-Kit/templates/TrueFocus/TrueFocus.jsx +64 -0
- package/dist/ApexUI-Kit/templates/TypewriterText/TypewriterText.jsx +70 -0
- package/dist/cli/index.js +43 -0
- package/package.json +1 -1
|
@@ -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, {
|
|
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.
|
|
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'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;
|
|
@@ -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,30 @@
|
|
|
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 text keeps its own color between sweeps.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const ShinyText = ({ text = "Shipping soon — join the waitlist", as = "span", speed = 2.8, color = "#71717a", shineColor = "#ffffff", spread = 22, disabled = false, className = "" }) => {
|
|
10
|
+
const reduced = useReducedMotion();
|
|
11
|
+
const Tag = motion[as] ?? motion.span;
|
|
12
|
+
const still = disabled || reduced;
|
|
13
|
+
return (
|
|
14
|
+
<Tag
|
|
15
|
+
className={`inline-block bg-clip-text text-transparent ${className}`}
|
|
16
|
+
style={{
|
|
17
|
+
backgroundImage: `linear-gradient(110deg, ${color} ${50 - spread}%, ${shineColor} 50%, ${color} ${50 + spread}%)`,
|
|
18
|
+
backgroundSize: "250% 100%",
|
|
19
|
+
WebkitBackgroundClip: "text",
|
|
20
|
+
}}
|
|
21
|
+
initial={{ backgroundPosition: "100% 0" }}
|
|
22
|
+
animate={still ? { backgroundPosition: "100% 0" } : { backgroundPosition: ["100% 0", "0% 0"] }}
|
|
23
|
+
transition={still ? undefined : { duration: speed, ease: "linear", repeat: Infinity, repeatDelay: speed * 0.25 }}
|
|
24
|
+
>
|
|
25
|
+
{text}
|
|
26
|
+
</Tag>
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
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;
|