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.
- 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 +12 -4
- 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 +74 -0
- package/dist/ApexUI-Kit/templates/Coverflow3D/Coverflow3D.jsx +108 -0
- package/dist/ApexUI-Kit/templates/CubeGallery/CubeGallery.jsx +168 -0
- package/dist/ApexUI-Kit/templates/CursorTrail/CursorTrail.jsx +200 -0
- package/dist/ApexUI-Kit/templates/DotGlobe/DotGlobe.jsx +1 -0
- package/dist/ApexUI-Kit/templates/DynamicIsland/DynamicIsland.jsx +1 -1
- package/dist/ApexUI-Kit/templates/ElasticSlider/ElasticSlider.jsx +161 -0
- package/dist/ApexUI-Kit/templates/ExtrudedText/ExtrudedText.jsx +78 -0
- package/dist/ApexUI-Kit/templates/FaqAccordion/FaqAccordion.jsx +1 -1
- 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 +312 -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 +97 -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/PointerFollower/PointerFollower.jsx +1 -1
- 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 +36 -0
- package/dist/ApexUI-Kit/templates/StarBorder/StarBorder.jsx +33 -0
- package/dist/ApexUI-Kit/templates/Stepper/Stepper.jsx +142 -0
- package/dist/ApexUI-Kit/templates/SwipeCards/SwipeCards.jsx +123 -0
- package/dist/ApexUI-Kit/templates/TextRing3D/TextRing3D.jsx +75 -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
- package/dist/ApexUI-Kit/templates/Carousel/DragCarousel.jsx +0 -105
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Gooey Nav: the active pill is drawn in WebGL as a metaball. A fragment
|
|
5
|
+
* shader smooth-unions a capsule (the pill) with a handful of droplets, so
|
|
6
|
+
* they genuinely melt into each other. When you switch links the pill's
|
|
7
|
+
* leading edge races ahead and the trailing edge catches up (it stretches,
|
|
8
|
+
* thins, then snaps back) and droplets burst out and get pulled back in.
|
|
9
|
+
* Labels are plain HTML on top, so they stay crisp and accessible. Falls
|
|
10
|
+
* back to a CSS pill without WebGL.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const DEFAULT_ITEMS = [
|
|
14
|
+
{ label: "Home", href: "#" },
|
|
15
|
+
{ label: "Components", href: "#" },
|
|
16
|
+
{ label: "Templates", href: "#" },
|
|
17
|
+
{ label: "Docs", href: "#" },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const MAX_DROPS = 14;
|
|
21
|
+
const PAD = 22; // canvas bleed around the bar, for drops and glow (px)
|
|
22
|
+
|
|
23
|
+
const VERT = `
|
|
24
|
+
attribute vec2 aPos;
|
|
25
|
+
void main() { gl_Position = vec4(aPos, 0.0, 1.0); }
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
const FRAG = `
|
|
29
|
+
precision highp float;
|
|
30
|
+
uniform vec2 uRes;
|
|
31
|
+
uniform float uDpr;
|
|
32
|
+
uniform vec4 uPill;
|
|
33
|
+
uniform vec4 uDrops[${MAX_DROPS}];
|
|
34
|
+
uniform vec3 uColor;
|
|
35
|
+
uniform float uGoo;
|
|
36
|
+
uniform float uGlow;
|
|
37
|
+
|
|
38
|
+
float sdCapsule(vec2 p, vec2 halfSize) {
|
|
39
|
+
float r = min(halfSize.x, halfSize.y);
|
|
40
|
+
vec2 q = abs(p) - halfSize + r;
|
|
41
|
+
return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
float smin(float a, float b, float k) {
|
|
45
|
+
float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
|
|
46
|
+
return mix(b, a, h) - k * h * (1.0 - h);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
void main() {
|
|
50
|
+
// Pixel position in CSS px, origin top-left.
|
|
51
|
+
vec2 p = vec2(gl_FragCoord.x, uRes.y - gl_FragCoord.y) / uDpr;
|
|
52
|
+
vec2 c = uPill.xy + uPill.zw * 0.5;
|
|
53
|
+
float d = sdCapsule(p - c, uPill.zw * 0.5);
|
|
54
|
+
for (int i = 0; i < ${MAX_DROPS}; i++) {
|
|
55
|
+
vec4 dr = uDrops[i];
|
|
56
|
+
if (dr.z > 0.01) d = smin(d, length(p - dr.xy) - dr.z, uGoo);
|
|
57
|
+
}
|
|
58
|
+
float aa = 0.8;
|
|
59
|
+
float body = 1.0 - smoothstep(-aa, aa, d);
|
|
60
|
+
// Soft top highlight and darker bottom for a bit of volume.
|
|
61
|
+
float v = clamp((p.y - uPill.y) / max(uPill.w, 1.0), 0.0, 1.0);
|
|
62
|
+
vec3 col = uColor * (1.08 - 0.18 * v);
|
|
63
|
+
float rim = smoothstep(-3.0, 0.0, d) * body * 0.12;
|
|
64
|
+
col += rim;
|
|
65
|
+
float glow = exp(-max(d, 0.0) / 5.0) * uGlow * (1.0 - body);
|
|
66
|
+
float a = body + glow;
|
|
67
|
+
gl_FragColor = vec4(col * a, a);
|
|
68
|
+
}
|
|
69
|
+
`;
|
|
70
|
+
|
|
71
|
+
const hexToRgb = (hex) => {
|
|
72
|
+
let h = String(hex).replace("#", "");
|
|
73
|
+
if (h.length === 3) h = h.split("").map((c) => c + c).join("");
|
|
74
|
+
const n = parseInt(h.slice(0, 6), 16);
|
|
75
|
+
if (Number.isNaN(n)) return [0.64, 0.9, 0.21];
|
|
76
|
+
return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
function createGL(canvas) {
|
|
80
|
+
const gl = canvas.getContext("webgl", { premultipliedAlpha: true, antialias: false, alpha: true });
|
|
81
|
+
if (!gl) return null;
|
|
82
|
+
const compile = (type, src) => {
|
|
83
|
+
const s = gl.createShader(type);
|
|
84
|
+
gl.shaderSource(s, src);
|
|
85
|
+
gl.compileShader(s);
|
|
86
|
+
return gl.getShaderParameter(s, gl.COMPILE_STATUS) ? s : null;
|
|
87
|
+
};
|
|
88
|
+
const vs = compile(gl.VERTEX_SHADER, VERT);
|
|
89
|
+
const fs = compile(gl.FRAGMENT_SHADER, FRAG);
|
|
90
|
+
if (!vs || !fs) return null;
|
|
91
|
+
const prog = gl.createProgram();
|
|
92
|
+
gl.attachShader(prog, vs);
|
|
93
|
+
gl.attachShader(prog, fs);
|
|
94
|
+
gl.linkProgram(prog);
|
|
95
|
+
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) return null;
|
|
96
|
+
gl.useProgram(prog);
|
|
97
|
+
const buf = gl.createBuffer();
|
|
98
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
|
|
99
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);
|
|
100
|
+
const loc = gl.getAttribLocation(prog, "aPos");
|
|
101
|
+
gl.enableVertexAttribArray(loc);
|
|
102
|
+
gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);
|
|
103
|
+
const u = (n) => gl.getUniformLocation(prog, n);
|
|
104
|
+
return { gl, uRes: u("uRes"), uDpr: u("uDpr"), uPill: u("uPill"), uDrops: u("uDrops"), uColor: u("uColor"), uGoo: u("uGoo"), uGlow: u("uGlow") };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// One spring integration step.
|
|
108
|
+
const step = (s, target, k, c, dt) => {
|
|
109
|
+
const a = k * (target - s.x) - c * s.v;
|
|
110
|
+
s.v += a * dt;
|
|
111
|
+
s.x += s.v * dt;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const GooeyNav = ({ items = DEFAULT_ITEMS, initialActive = 0, onChange, accentColor = "#a3e635", particleCount = 8, goo = 12, theme = "dark", className = "" }) => {
|
|
115
|
+
const [active, setActive] = useState(initialActive);
|
|
116
|
+
const [fallback, setFallback] = useState(false);
|
|
117
|
+
const bar = useRef(null);
|
|
118
|
+
const canvas = useRef(null);
|
|
119
|
+
const fallbackPill = useRef(null);
|
|
120
|
+
const links = useRef([]);
|
|
121
|
+
const sim = useRef(null);
|
|
122
|
+
const color = useRef(hexToRgb(accentColor));
|
|
123
|
+
const gooRef = useRef(goo);
|
|
124
|
+
const glowRef = useRef(theme === "light" ? 0 : 0.22);
|
|
125
|
+
const dark = theme !== "light";
|
|
126
|
+
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
color.current = hexToRgb(accentColor);
|
|
129
|
+
gooRef.current = goo;
|
|
130
|
+
glowRef.current = theme === "light" ? 0 : 0.22;
|
|
131
|
+
if (sim.current) sim.current.dirty = true;
|
|
132
|
+
}, [accentColor, goo, theme]);
|
|
133
|
+
|
|
134
|
+
const measure = (i) => {
|
|
135
|
+
const el = links.current[i];
|
|
136
|
+
const b = bar.current;
|
|
137
|
+
if (!el || !b) return null;
|
|
138
|
+
const a = el.getBoundingClientRect();
|
|
139
|
+
const p = b.getBoundingClientRect();
|
|
140
|
+
return { l: a.left - p.left + PAD, r: a.right - p.left + PAD, t: a.top - p.top + PAD, h: a.height };
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// Set up GL + simulation once.
|
|
144
|
+
useLayoutEffect(() => {
|
|
145
|
+
const cv = canvas.current;
|
|
146
|
+
const ctx = createGL(cv);
|
|
147
|
+
if (!ctx) setFallback(true);
|
|
148
|
+
const m = measure(active) ?? { l: 0, r: 0, t: 0, h: 0 };
|
|
149
|
+
const s = {
|
|
150
|
+
l: { x: m.l, v: 0 },
|
|
151
|
+
r: { x: m.r, v: 0 },
|
|
152
|
+
t: { x: m.t, v: 0 },
|
|
153
|
+
h: { x: m.h, v: 0 },
|
|
154
|
+
target: m,
|
|
155
|
+
activeIndex: active,
|
|
156
|
+
dir: 1,
|
|
157
|
+
drops: [],
|
|
158
|
+
dirty: true,
|
|
159
|
+
dpr: 1,
|
|
160
|
+
};
|
|
161
|
+
sim.current = s;
|
|
162
|
+
|
|
163
|
+
// Hold the element itself: the ref is cleared on unmount before cleanup runs.
|
|
164
|
+
const barEl = bar.current;
|
|
165
|
+
const resize = () => {
|
|
166
|
+
const r = barEl.getBoundingClientRect();
|
|
167
|
+
s.dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
168
|
+
const w = r.width + PAD * 2;
|
|
169
|
+
const h = r.height + PAD * 2;
|
|
170
|
+
cv.width = Math.round(w * s.dpr);
|
|
171
|
+
cv.height = Math.round(h * s.dpr);
|
|
172
|
+
cv.style.width = `${w}px`;
|
|
173
|
+
cv.style.height = `${h}px`;
|
|
174
|
+
const t = measure(s.activeIndex);
|
|
175
|
+
if (t) {
|
|
176
|
+
s.target = t;
|
|
177
|
+
s.l.x = t.l;
|
|
178
|
+
s.r.x = t.r;
|
|
179
|
+
s.t.x = t.t;
|
|
180
|
+
s.h.x = t.h;
|
|
181
|
+
s.l.v = s.r.v = s.t.v = s.h.v = 0;
|
|
182
|
+
}
|
|
183
|
+
s.dirty = true;
|
|
184
|
+
};
|
|
185
|
+
resize();
|
|
186
|
+
const ro = new ResizeObserver(resize);
|
|
187
|
+
ro.observe(barEl);
|
|
188
|
+
|
|
189
|
+
const dropData = new Float32Array(MAX_DROPS * 4);
|
|
190
|
+
let raf = 0;
|
|
191
|
+
let last = performance.now();
|
|
192
|
+
const frame = (now) => {
|
|
193
|
+
const dt = Math.min(0.033, (now - last) / 1000);
|
|
194
|
+
last = now;
|
|
195
|
+
const T = s.target;
|
|
196
|
+
// Leading edge is stiff, trailing edge is loose → stretch.
|
|
197
|
+
const lead = [520, 30];
|
|
198
|
+
const trail = [170, 17];
|
|
199
|
+
const [kl, cl] = s.dir > 0 ? trail : lead;
|
|
200
|
+
const [kr, cr] = s.dir > 0 ? lead : trail;
|
|
201
|
+
step(s.l, T.l, kl, cl, dt);
|
|
202
|
+
step(s.r, T.r, kr, cr, dt);
|
|
203
|
+
step(s.t, T.t, 400, 32, dt);
|
|
204
|
+
step(s.h, T.h, 400, 32, dt);
|
|
205
|
+
|
|
206
|
+
// Thin the pill while it is stretched (keeps the "volume").
|
|
207
|
+
const w = s.r.x - s.l.x;
|
|
208
|
+
const restW = T.r - T.l || 1;
|
|
209
|
+
const stretch = Math.max(0, w / restW - 1);
|
|
210
|
+
const h = s.h.x * (1 - Math.min(0.32, stretch * 0.35));
|
|
211
|
+
const top = s.t.x + (s.h.x - h) / 2;
|
|
212
|
+
|
|
213
|
+
// Droplets fly out, then a spring pulls them back to the pill centre.
|
|
214
|
+
const cx = (s.l.x + s.r.x) / 2;
|
|
215
|
+
const cy = top + h / 2;
|
|
216
|
+
s.drops = s.drops.filter((d) => (d.life -= dt) > 0);
|
|
217
|
+
for (const d of s.drops) {
|
|
218
|
+
d.vx += ((cx - d.x) * d.k - d.vx * 5) * dt;
|
|
219
|
+
d.vy += ((cy - d.y) * d.k - d.vy * 5) * dt;
|
|
220
|
+
d.x += d.vx * dt;
|
|
221
|
+
d.y += d.vy * dt;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const moving = s.drops.length > 0 || [s.l, s.r, s.t, s.h].some((q) => Math.abs(q.v) > 0.5) || Math.abs(s.l.x - T.l) > 0.3 || Math.abs(s.r.x - T.r) > 0.3;
|
|
225
|
+
if (moving || s.dirty) {
|
|
226
|
+
s.dirty = false;
|
|
227
|
+
if (ctx) {
|
|
228
|
+
const { gl } = ctx;
|
|
229
|
+
gl.viewport(0, 0, cv.width, cv.height);
|
|
230
|
+
gl.clearColor(0, 0, 0, 0);
|
|
231
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
232
|
+
gl.uniform2f(ctx.uRes, cv.width, cv.height);
|
|
233
|
+
gl.uniform1f(ctx.uDpr, s.dpr);
|
|
234
|
+
gl.uniform4f(ctx.uPill, s.l.x, top, Math.max(1, w), Math.max(1, h));
|
|
235
|
+
dropData.fill(0);
|
|
236
|
+
s.drops.slice(0, MAX_DROPS).forEach((d, i) => {
|
|
237
|
+
const life = Math.max(0, d.life / d.max);
|
|
238
|
+
dropData.set([d.x, d.y, d.r * Math.min(1, life * 1.6), 1], i * 4);
|
|
239
|
+
});
|
|
240
|
+
gl.uniform4fv(ctx.uDrops, dropData);
|
|
241
|
+
gl.uniform3fv(ctx.uColor, color.current);
|
|
242
|
+
gl.uniform1f(ctx.uGoo, gooRef.current);
|
|
243
|
+
gl.uniform1f(ctx.uGlow, glowRef.current);
|
|
244
|
+
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
|
245
|
+
} else if (fallbackPill.current) {
|
|
246
|
+
Object.assign(fallbackPill.current.style, { left: `${s.l.x - PAD}px`, top: `${top - PAD}px`, width: `${w}px`, height: `${h}px` });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
raf = requestAnimationFrame(frame);
|
|
250
|
+
};
|
|
251
|
+
raf = requestAnimationFrame(frame);
|
|
252
|
+
return () => {
|
|
253
|
+
cancelAnimationFrame(raf);
|
|
254
|
+
ro.disconnect();
|
|
255
|
+
ctx?.gl.getExtension("WEBGL_lose_context")?.loseContext();
|
|
256
|
+
};
|
|
257
|
+
// Built once per item count; later selections go through sim.current.
|
|
258
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
259
|
+
}, [items.length]);
|
|
260
|
+
|
|
261
|
+
const select = (i, e) => {
|
|
262
|
+
e?.preventDefault();
|
|
263
|
+
const s = sim.current;
|
|
264
|
+
if (i === active || !s) return;
|
|
265
|
+
const t = measure(i);
|
|
266
|
+
if (!t) return;
|
|
267
|
+
s.dir = i > active ? 1 : -1;
|
|
268
|
+
s.target = t;
|
|
269
|
+
s.activeIndex = i;
|
|
270
|
+
setActive(i);
|
|
271
|
+
onChange?.(i, items[i]);
|
|
272
|
+
if (window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) {
|
|
273
|
+
s.l.x = t.l;
|
|
274
|
+
s.r.x = t.r;
|
|
275
|
+
s.dirty = true;
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
const cx = (t.l + t.r) / 2;
|
|
279
|
+
const cy = t.t + t.h / 2;
|
|
280
|
+
// Burst at the destination; the droplets get pulled back into the pill.
|
|
281
|
+
for (let k = 0; k < particleCount; k++) {
|
|
282
|
+
const a = (Math.PI * 2 * k) / particleCount + (Math.random() - 0.5) * 0.6;
|
|
283
|
+
const sp = 120 + Math.random() * 110;
|
|
284
|
+
const max = 0.75 + Math.random() * 0.3;
|
|
285
|
+
s.drops.push({ x: cx, y: cy, vx: Math.cos(a) * sp * 1.5, vy: Math.sin(a) * sp * 0.55, r: 4 + Math.random() * 4, k: 55, life: max, max });
|
|
286
|
+
}
|
|
287
|
+
if (s.drops.length > MAX_DROPS) s.drops.splice(0, s.drops.length - MAX_DROPS);
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
return (
|
|
291
|
+
<nav className={`relative inline-flex max-w-full ${className}`} aria-label="Main">
|
|
292
|
+
<div ref={bar} className={`relative flex items-center gap-0.5 rounded-full p-1.5 ${dark ? "bg-neutral-950 ring-1 ring-white/10" : "bg-white shadow-sm ring-1 ring-black/10"}`}>
|
|
293
|
+
<canvas ref={canvas} aria-hidden="true" className="pointer-events-none absolute" style={{ left: -PAD, top: -PAD, display: fallback ? "none" : "block" }} />
|
|
294
|
+
{fallback && <span ref={fallbackPill} aria-hidden="true" className="pointer-events-none absolute rounded-full" style={{ background: accentColor }} />}
|
|
295
|
+
{items.map((item, i) => (
|
|
296
|
+
<a
|
|
297
|
+
key={i}
|
|
298
|
+
ref={(el) => (links.current[i] = el)}
|
|
299
|
+
href={item.href}
|
|
300
|
+
onClick={(e) => select(i, e)}
|
|
301
|
+
aria-current={i === active ? "page" : undefined}
|
|
302
|
+
className={`relative z-[1] shrink-0 rounded-full px-3.5 py-2.5 text-[0.95rem] font-medium outline-none transition-colors duration-300 focus-visible:ring-2 focus-visible:ring-current/30 sm:px-5 ${i === active ? "text-neutral-950" : dark ? "text-white/65 hover:text-white" : "text-neutral-500 hover:text-neutral-900"}`}
|
|
303
|
+
>
|
|
304
|
+
{item.label}
|
|
305
|
+
</a>
|
|
306
|
+
))}
|
|
307
|
+
</div>
|
|
308
|
+
</nav>
|
|
309
|
+
);
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
export default GooeyNav;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { useReducedMotion } from "framer-motion";
|
|
3
|
+
import { Check } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* Hold to Confirm: for actions you don't want to trigger by accident. Press
|
|
7
|
+
* and hold (pointer, Enter or Space) and a solid copy of the button wipes
|
|
8
|
+
* in from the left; let go, slide off the button or lose focus and it snaps
|
|
9
|
+
* back. Once the wipe finishes it confirms and resets after `resetAfter`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const HoldToConfirm = ({ children = "Hold to confirm", className = "", confirmedContent, disabled, duration = 1600, onConfirm, resetAfter = 1800, ...buttonProps }) => {
|
|
13
|
+
const buttonRef = useRef(null);
|
|
14
|
+
const confirmTimerRef = useRef(null);
|
|
15
|
+
const resetTimerRef = useRef(null);
|
|
16
|
+
const activePointerIdRef = useRef(null);
|
|
17
|
+
const inputModeRef = useRef("pointer");
|
|
18
|
+
const isHoldingRef = useRef(false);
|
|
19
|
+
const [inputMode, setInputMode] = useState(null);
|
|
20
|
+
const [status, setStatus] = useState("idle"); // idle | holding | confirmed
|
|
21
|
+
const shouldReduceMotion = useReducedMotion();
|
|
22
|
+
|
|
23
|
+
const clearConfirmTimer = useCallback(() => {
|
|
24
|
+
if (confirmTimerRef.current === null) return;
|
|
25
|
+
window.clearTimeout(confirmTimerRef.current);
|
|
26
|
+
confirmTimerRef.current = null;
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
const completeHold = useCallback(() => {
|
|
30
|
+
if (!isHoldingRef.current) return;
|
|
31
|
+
isHoldingRef.current = false;
|
|
32
|
+
activePointerIdRef.current = null;
|
|
33
|
+
clearConfirmTimer();
|
|
34
|
+
setStatus("confirmed");
|
|
35
|
+
onConfirm?.(inputModeRef.current);
|
|
36
|
+
if (resetAfter > 0) {
|
|
37
|
+
resetTimerRef.current = window.setTimeout(() => {
|
|
38
|
+
setStatus("idle");
|
|
39
|
+
resetTimerRef.current = null;
|
|
40
|
+
}, resetAfter);
|
|
41
|
+
}
|
|
42
|
+
}, [clearConfirmTimer, onConfirm, resetAfter]);
|
|
43
|
+
|
|
44
|
+
const cancelHold = useCallback(() => {
|
|
45
|
+
if (!isHoldingRef.current) return;
|
|
46
|
+
isHoldingRef.current = false;
|
|
47
|
+
activePointerIdRef.current = null;
|
|
48
|
+
clearConfirmTimer();
|
|
49
|
+
setStatus("idle");
|
|
50
|
+
}, [clearConfirmTimer]);
|
|
51
|
+
|
|
52
|
+
const startHold = useCallback(
|
|
53
|
+
(input) => {
|
|
54
|
+
if (disabled || status === "confirmed" || isHoldingRef.current) return;
|
|
55
|
+
if (resetTimerRef.current !== null) {
|
|
56
|
+
window.clearTimeout(resetTimerRef.current);
|
|
57
|
+
resetTimerRef.current = null;
|
|
58
|
+
}
|
|
59
|
+
inputModeRef.current = input;
|
|
60
|
+
setInputMode(input);
|
|
61
|
+
isHoldingRef.current = true;
|
|
62
|
+
setStatus("holding");
|
|
63
|
+
confirmTimerRef.current = window.setTimeout(completeHold, duration);
|
|
64
|
+
},
|
|
65
|
+
[completeHold, disabled, duration, status],
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
useEffect(
|
|
69
|
+
() => () => {
|
|
70
|
+
clearConfirmTimer();
|
|
71
|
+
if (resetTimerRef.current !== null) window.clearTimeout(resetTimerRef.current);
|
|
72
|
+
},
|
|
73
|
+
[clearConfirmTimer],
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (disabled) cancelHold();
|
|
78
|
+
}, [cancelHold, disabled]);
|
|
79
|
+
|
|
80
|
+
const releasePointerCapture = (pointerId) => {
|
|
81
|
+
const button = buttonRef.current;
|
|
82
|
+
if (button?.hasPointerCapture(pointerId)) button.releasePointerCapture(pointerId);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const handlePointerDown = (event) => {
|
|
86
|
+
if (!event.isPrimary || event.button !== 0 || disabled) return;
|
|
87
|
+
activePointerIdRef.current = event.pointerId;
|
|
88
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
89
|
+
startHold("pointer");
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const handlePointerMove = (event) => {
|
|
93
|
+
if (!isHoldingRef.current || activePointerIdRef.current !== event.pointerId) return;
|
|
94
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
95
|
+
const boundaryPadding = 8;
|
|
96
|
+
const isOutside = event.clientX < rect.left - boundaryPadding || event.clientX > rect.right + boundaryPadding || event.clientY < rect.top - boundaryPadding || event.clientY > rect.bottom + boundaryPadding;
|
|
97
|
+
if (isOutside) {
|
|
98
|
+
cancelHold();
|
|
99
|
+
releasePointerCapture(event.pointerId);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const handlePointerEnd = (event) => {
|
|
104
|
+
if (activePointerIdRef.current !== event.pointerId) return;
|
|
105
|
+
cancelHold();
|
|
106
|
+
releasePointerCapture(event.pointerId);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const handleKeyDown = (event) => {
|
|
110
|
+
if (event.repeat || (event.key !== "Enter" && event.key !== " ")) return;
|
|
111
|
+
event.preventDefault();
|
|
112
|
+
startHold("keyboard");
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const handleKeyUp = (event) => {
|
|
116
|
+
if (event.key !== "Enter" && event.key !== " ") return;
|
|
117
|
+
event.preventDefault();
|
|
118
|
+
cancelHold();
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const isConfirmed = status === "confirmed";
|
|
122
|
+
const isHolding = status === "holding";
|
|
123
|
+
const overlayStyle = {
|
|
124
|
+
clipPath: isHolding ? "inset(0 0 0 0)" : "inset(0 100% 0 0)",
|
|
125
|
+
transitionDuration: shouldReduceMotion || inputMode === "keyboard" ? "0ms" : isHolding ? `${duration}ms` : "180ms",
|
|
126
|
+
transitionProperty: "clip-path",
|
|
127
|
+
transitionTimingFunction: isHolding ? "linear" : "cubic-bezier(0.23, 1, 0.32, 1)",
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<button
|
|
132
|
+
{...buttonProps}
|
|
133
|
+
aria-busy={isHolding}
|
|
134
|
+
className={`relative isolate inline-flex h-9 min-w-40 cursor-pointer touch-none select-none items-center justify-center overflow-hidden rounded-lg border border-red-500 px-3 text-sm font-medium outline-none transition-[transform,border-color,background-color,color] duration-150 [transition-timing-function:cubic-bezier(0.23,1,0.32,1)] focus-visible:ring-2 focus-visible:ring-red-400 focus-visible:ring-offset-2 focus-visible:ring-offset-transparent data-[input=pointer]:active:scale-[0.98] disabled:cursor-not-allowed disabled:opacity-50 ${isConfirmed ? "bg-red-500 text-white" : "bg-red-500/10 text-red-500"} ${className}`}
|
|
135
|
+
data-input={inputMode ?? undefined}
|
|
136
|
+
disabled={disabled}
|
|
137
|
+
onBlur={cancelHold}
|
|
138
|
+
onKeyDown={handleKeyDown}
|
|
139
|
+
onKeyUp={handleKeyUp}
|
|
140
|
+
onLostPointerCapture={cancelHold}
|
|
141
|
+
onPointerCancel={handlePointerEnd}
|
|
142
|
+
onPointerDown={handlePointerDown}
|
|
143
|
+
onPointerMove={handlePointerMove}
|
|
144
|
+
onPointerUp={handlePointerEnd}
|
|
145
|
+
ref={buttonRef}
|
|
146
|
+
type="button"
|
|
147
|
+
>
|
|
148
|
+
{isConfirmed ? (
|
|
149
|
+
<span className="relative flex items-center justify-center gap-1.5">
|
|
150
|
+
{confirmedContent ?? (
|
|
151
|
+
<>
|
|
152
|
+
<Check aria-hidden="true" className="h-[15px] w-[15px]" strokeWidth={3} />
|
|
153
|
+
Confirmed
|
|
154
|
+
</>
|
|
155
|
+
)}
|
|
156
|
+
</span>
|
|
157
|
+
) : (
|
|
158
|
+
<>
|
|
159
|
+
<span className="relative flex items-center justify-center gap-1.5">{children}</span>
|
|
160
|
+
<span aria-hidden="true" className="absolute inset-0 flex items-center justify-center gap-1.5 bg-red-500 px-3 text-white" style={overlayStyle}>
|
|
161
|
+
{children}
|
|
162
|
+
</span>
|
|
163
|
+
</>
|
|
164
|
+
)}
|
|
165
|
+
</button>
|
|
166
|
+
);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export default HoldToConfirm;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React, { useEffect, useState } from "react";
|
|
2
|
+
import { useReducedMotion } from "framer-motion";
|
|
3
|
+
import { CheckCircle2, RotateCcw } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* Undo Notice: pairs with HoldToConfirm. Shows what just happened with an
|
|
7
|
+
* Undo button whose solid fill drains away over `duration`; when it runs
|
|
8
|
+
* out, onExpire fires. onUndo tells you if it came from a key or a pointer.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const UndoNotice = ({ className = "", duration = 5000, message = "Project deleted", onExpire, onUndo }) => {
|
|
12
|
+
const [isCounting, setIsCounting] = useState(false);
|
|
13
|
+
const shouldReduceMotion = useReducedMotion();
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const frame = window.requestAnimationFrame(() => setIsCounting(true));
|
|
17
|
+
const timer = window.setTimeout(() => onExpire?.(), duration);
|
|
18
|
+
return () => {
|
|
19
|
+
window.cancelAnimationFrame(frame);
|
|
20
|
+
window.clearTimeout(timer);
|
|
21
|
+
};
|
|
22
|
+
}, [duration, onExpire]);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div aria-live="polite" role="status" className={`flex min-h-9 w-full max-w-xs items-center justify-between gap-3 rounded-xl bg-neutral-100 p-1 pl-3 dark:bg-neutral-800 ${className}`}>
|
|
26
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
27
|
+
<CheckCircle2 aria-hidden="true" className="h-4 w-4 shrink-0 fill-green-500 text-white dark:text-neutral-800" />
|
|
28
|
+
<p className="truncate text-xs font-medium text-neutral-900 dark:text-neutral-100">{message}</p>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<button
|
|
32
|
+
type="button"
|
|
33
|
+
onClick={(event) => onUndo?.(event.detail === 0 ? "keyboard" : "pointer")}
|
|
34
|
+
className="relative isolate flex h-7 min-w-20 cursor-pointer items-center justify-center overflow-hidden rounded-lg border border-neutral-200 bg-white px-2.5 text-xs font-medium text-neutral-600 outline-none transition-transform duration-150 [transition-timing-function:cubic-bezier(0.23,1,0.32,1)] focus-visible:ring-2 focus-visible:ring-neutral-400 active:scale-[0.97] dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-300"
|
|
35
|
+
>
|
|
36
|
+
<span className="flex items-center gap-1.5">
|
|
37
|
+
<RotateCcw aria-hidden="true" className="h-[13px] w-[13px]" strokeWidth={2.5} />
|
|
38
|
+
Undo
|
|
39
|
+
</span>
|
|
40
|
+
<span
|
|
41
|
+
aria-hidden="true"
|
|
42
|
+
className="absolute inset-0 flex items-center justify-center gap-1.5 bg-neutral-900 px-2.5 text-white dark:bg-neutral-100 dark:text-neutral-900"
|
|
43
|
+
style={{
|
|
44
|
+
clipPath: isCounting ? "inset(0 100% 0 0)" : "inset(0 0 0 0)",
|
|
45
|
+
transitionDuration: shouldReduceMotion ? "0ms" : `${duration}ms`,
|
|
46
|
+
transitionProperty: "clip-path",
|
|
47
|
+
transitionTimingFunction: "linear",
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
<RotateCcw aria-hidden="true" className="h-[13px] w-[13px]" strokeWidth={2.5} />
|
|
51
|
+
Undo
|
|
52
|
+
</span>
|
|
53
|
+
</button>
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export default UndoNotice;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import React, { useRef } from "react";
|
|
2
|
+
import { motion, useMotionTemplate, useMotionValue, useReducedMotion, useSpring, useTransform } from "framer-motion";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Holo Card: a collectible-style card that tilts in 3D under the pointer.
|
|
6
|
+
* A rainbow foil and a glare follow the light, and the artwork, title and
|
|
7
|
+
* badge float at different depths so the card feels thick.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const IK = "https://ik.imagekit.io/ny6yinyut/apexui/assets";
|
|
11
|
+
|
|
12
|
+
const HoloCard = ({
|
|
13
|
+
image = `${IK}/doctorStrange.png?tr=f-auto,q-80,w-600`,
|
|
14
|
+
title = "Doctor Strange",
|
|
15
|
+
subtitle = "Sorcerer Supreme",
|
|
16
|
+
badge = "Legendary",
|
|
17
|
+
stat = "98",
|
|
18
|
+
accentColor = "#a3e635",
|
|
19
|
+
maxTilt = 16,
|
|
20
|
+
width = 17,
|
|
21
|
+
className = "",
|
|
22
|
+
}) => {
|
|
23
|
+
const ref = useRef(null);
|
|
24
|
+
const reduced = useReducedMotion();
|
|
25
|
+
const px = useMotionValue(0.5);
|
|
26
|
+
const py = useMotionValue(0.5);
|
|
27
|
+
const spring = { stiffness: 180, damping: 18, mass: 0.6 };
|
|
28
|
+
const sx = useSpring(px, spring);
|
|
29
|
+
const sy = useSpring(py, spring);
|
|
30
|
+
const rotateY = useTransform(sx, [0, 1], [-maxTilt, maxTilt]);
|
|
31
|
+
const rotateX = useTransform(sy, [0, 1], [maxTilt, -maxTilt]);
|
|
32
|
+
const glareX = useTransform(sx, (v) => `${v * 100}%`);
|
|
33
|
+
const glareY = useTransform(sy, (v) => `${v * 100}%`);
|
|
34
|
+
const foilPos = useTransform([sx, sy], ([x, y]) => `${x * 100}% ${y * 100}%`);
|
|
35
|
+
const glare = useMotionTemplate`radial-gradient(circle at ${glareX} ${glareY}, rgba(255,255,255,0.55), rgba(255,255,255,0) 45%)`;
|
|
36
|
+
const shadow = useTransform([sx, sy], ([x, y]) => `${(0.5 - x) * 40}px ${(0.5 - y) * 40 + 30}px 60px -20px rgba(0,0,0,0.6)`);
|
|
37
|
+
|
|
38
|
+
const onMove = (e) => {
|
|
39
|
+
if (reduced) return;
|
|
40
|
+
const r = ref.current.getBoundingClientRect();
|
|
41
|
+
px.set((e.clientX - r.left) / r.width);
|
|
42
|
+
py.set((e.clientY - r.top) / r.height);
|
|
43
|
+
};
|
|
44
|
+
const onLeave = () => {
|
|
45
|
+
px.set(0.5);
|
|
46
|
+
py.set(0.5);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div className={`[perspective:1100px] ${className}`}>
|
|
51
|
+
<motion.article
|
|
52
|
+
ref={ref}
|
|
53
|
+
onPointerMove={onMove}
|
|
54
|
+
onPointerLeave={onLeave}
|
|
55
|
+
className="relative aspect-[5/7] cursor-pointer select-none rounded-[1.4rem] bg-neutral-900 [transform-style:preserve-3d]"
|
|
56
|
+
style={{ width: `min(${width}rem, 80vw)`, rotateX, rotateY, boxShadow: shadow }}
|
|
57
|
+
whileHover={{ scale: 1.03 }}
|
|
58
|
+
transition={{ type: "spring", stiffness: 260, damping: 20 }}
|
|
59
|
+
>
|
|
60
|
+
<div className="absolute inset-0 overflow-hidden rounded-[1.4rem] ring-1 ring-white/15">
|
|
61
|
+
<img src={image} alt="" draggable="false" className="h-full w-full object-cover" />
|
|
62
|
+
{/* Holographic foil: a rainbow that slides with the light. */}
|
|
63
|
+
<motion.div
|
|
64
|
+
aria-hidden="true"
|
|
65
|
+
className="absolute inset-0 opacity-[0.22] mix-blend-color-dodge"
|
|
66
|
+
style={{
|
|
67
|
+
backgroundImage: "linear-gradient(115deg, transparent 20%, #ff7ad9 30%, #7afcff 40%, #fdff7a 50%, #7aff9b 60%, #b57aff 70%, transparent 80%)",
|
|
68
|
+
backgroundSize: "260% 260%",
|
|
69
|
+
backgroundPosition: foilPos,
|
|
70
|
+
}}
|
|
71
|
+
/>
|
|
72
|
+
<motion.div aria-hidden="true" className="absolute inset-0 mix-blend-overlay" style={{ background: glare }} />
|
|
73
|
+
<div className="absolute inset-x-0 bottom-0 h-1/2 bg-gradient-to-t from-black/85 to-transparent" />
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
{/* Layers that float above the card. */}
|
|
77
|
+
<div className="pointer-events-none absolute inset-x-4 top-4 flex items-center justify-between [transform:translateZ(40px)]">
|
|
78
|
+
<span className="rounded-full px-2.5 py-1 text-[0.65rem] font-bold uppercase tracking-[0.18em] text-black shadow-lg" style={{ background: accentColor }}>
|
|
79
|
+
{badge}
|
|
80
|
+
</span>
|
|
81
|
+
<span className="grid h-10 w-10 place-items-center rounded-full border border-white/30 bg-black/50 font-mono text-sm font-bold text-white backdrop-blur">{stat}</span>
|
|
82
|
+
</div>
|
|
83
|
+
<div className="pointer-events-none absolute inset-x-5 bottom-5 text-white [transform:translateZ(60px)]">
|
|
84
|
+
<p className="text-[1.35rem] font-bold leading-tight drop-shadow-lg">{title}</p>
|
|
85
|
+
<p className="text-[0.82rem] text-white/70">{subtitle}</p>
|
|
86
|
+
</div>
|
|
87
|
+
</motion.article>
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export default HoloCard;
|