apex-ui-kit 2.6.0 → 2.8.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/ReadMe.md +32 -10
- package/dist/ApexUI-Kit/templates/AccordionMarquee/AccordionMarquee.jsx +340 -161
- package/dist/ApexUI-Kit/templates/AnimatedTabs/AnimatedTabs.jsx +1 -1
- package/dist/ApexUI-Kit/templates/ArcGallery/ArcGallery.jsx +356 -0
- package/dist/ApexUI-Kit/templates/BounceCards/BounceCards.jsx +22 -2
- package/dist/ApexUI-Kit/templates/CircularCarousel/CircularCarousel.jsx +381 -0
- package/dist/ApexUI-Kit/templates/CommandPalette/CommandPalette.jsx +250 -0
- package/dist/ApexUI-Kit/templates/CrossedMarquee/CrossedMarquee.jsx +158 -0
- package/dist/ApexUI-Kit/templates/DitherWaves/DitherWaves.jsx +207 -0
- package/dist/ApexUI-Kit/templates/EdgeBlur/EdgeBlur.jsx +92 -0
- package/dist/ApexUI-Kit/templates/FlipClock/FlipClock.jsx +5 -1
- package/dist/ApexUI-Kit/templates/GlassLens/GlassLens.jsx +274 -0
- package/dist/ApexUI-Kit/templates/GooeyNav/GooeyNav.jsx +2 -2
- package/dist/ApexUI-Kit/templates/HorizonGlow/HorizonGlow.jsx +259 -0
- package/dist/ApexUI-Kit/templates/ImageAccordion/ImageAccordion.jsx +116 -0
- package/dist/ApexUI-Kit/templates/InfiniteCanvas/InfiniteCanvas.jsx +230 -0
- package/dist/ApexUI-Kit/templates/MagneticButton/MagneticButton.jsx +69 -0
- package/dist/ApexUI-Kit/templates/Marquee3D/Marquee3D.jsx +68 -0
- package/dist/ApexUI-Kit/templates/NavDropDown/NavDropDown.jsx +123 -1
- package/dist/ApexUI-Kit/templates/OrbitIcons/OrbitIcons.jsx +169 -0
- package/dist/ApexUI-Kit/templates/ParticleText/ParticleText.jsx +328 -0
- package/dist/ApexUI-Kit/templates/RadialMenu/RadialMenu.jsx +119 -0
- package/dist/ApexUI-Kit/templates/ScrollStack/ScrollStack.jsx +88 -0
- package/dist/ApexUI-Kit/templates/SidebarNav/SidebarNav.jsx +22 -1
- package/dist/ApexUI-Kit/templates/SunriseBackground/SunriseBackground.jsx +290 -0
- package/dist/ApexUI-Kit/templates/TextRing3D/TextRing3D.jsx +18 -3
- package/dist/ApexUI-Kit/templates/ThemeSwitcher/ThemeSwitcher.jsx +215 -0
- package/dist/ApexUI-Kit/templates/ThemeSwitcher/themes.js +38 -0
- package/dist/ApexUI-Kit/templates/ToastStack/ToastStack.jsx +21 -13
- package/dist/ApexUI-Kit/templates/WireframeKnot3D/WireframeKnot3D.jsx +112 -0
- package/dist/cli/index.js +20 -0
- package/package.json +2 -2
- package/dist/ApexUI-Kit/templates/AccordionMarquee/AccordionMarquee.css +0 -1
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Infinite Canvas: an endless wall of image cards you can drag, flick or
|
|
5
|
+
* scroll in any direction. It glides to a stop with momentum, cards lean
|
|
6
|
+
* with the speed of the move and settle flat again, and the wall drifts on
|
|
7
|
+
* its own while nobody touches it. Only enough cards to cover the view are
|
|
8
|
+
* rendered; they are recycled as they leave an edge, so it stays light.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const IK = "https://ik.imagekit.io/ny6yinyut/apexui/assets/componentsimage";
|
|
12
|
+
const shot = (n, t) => ({ image: `${IK}/${n}.png?tr=f-auto,q-75,w-520`, title: t });
|
|
13
|
+
const DEFAULT_ITEMS = [
|
|
14
|
+
shot("galaxy-3d", "Galaxy 3D"),
|
|
15
|
+
shot("warp-tunnel", "Warp Tunnel"),
|
|
16
|
+
shot("liquid-blob-3d", "Liquid Blob 3D"),
|
|
17
|
+
shot("particle-text", "Particle Text"),
|
|
18
|
+
shot("retro-grid-3d", "Retro Grid 3D"),
|
|
19
|
+
shot("marquee-3d", "Marquee 3D"),
|
|
20
|
+
shot("sunrise-background", "Sunrise Background"),
|
|
21
|
+
shot("circular-carousel", "Circular Carousel"),
|
|
22
|
+
shot("wireframe-knot-3d", "Wireframe Knot 3D"),
|
|
23
|
+
shot("arc-gallery", "Arc Gallery"),
|
|
24
|
+
shot("particle-morph", "Particle Morph"),
|
|
25
|
+
shot("scroll-stack", "Scroll Stack"),
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const InfiniteCanvas = ({ items = DEFAULT_ITEMS, cardWidth = 14, aspectRatio = 0.8, gap = 1.25, drift = 18, tilt = 1, friction = 0.93, showTitles = true, height = "34rem", onItemClick, className = "" }) => {
|
|
29
|
+
const wrap = useRef(null);
|
|
30
|
+
const tiles = useRef([]);
|
|
31
|
+
const [pool, setPool] = useState(0);
|
|
32
|
+
const live = useRef({});
|
|
33
|
+
live.current = { drift, tilt, friction, showTitles, onItemClick };
|
|
34
|
+
const list = items.length ? items : DEFAULT_ITEMS;
|
|
35
|
+
const listRef = useRef(list);
|
|
36
|
+
listRef.current = list;
|
|
37
|
+
const geo = useRef({ cw: 1, ch: 1, cols: 0, rows: 0 });
|
|
38
|
+
|
|
39
|
+
// Size the pool to cover the view (plus a ring of spares) whenever it resizes.
|
|
40
|
+
useLayoutEffect(() => {
|
|
41
|
+
const el = wrap.current;
|
|
42
|
+
const fit = () => {
|
|
43
|
+
const { width, height: hh } = el.getBoundingClientRect();
|
|
44
|
+
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
|
|
45
|
+
// Cards shrink a little on small screens so a few always fit.
|
|
46
|
+
const w = Math.min(cardWidth * rem, Math.max(width * 0.42, 120));
|
|
47
|
+
const cw = w + gap * rem;
|
|
48
|
+
const ch = w / aspectRatio + gap * rem;
|
|
49
|
+
const cols = Math.ceil(width / cw) + 2;
|
|
50
|
+
const rows = Math.ceil(hh / ch) + 3;
|
|
51
|
+
geo.current = { cw, ch, w, h: w / aspectRatio, cols, rows };
|
|
52
|
+
setPool(cols * rows);
|
|
53
|
+
};
|
|
54
|
+
fit();
|
|
55
|
+
const ro = new ResizeObserver(fit);
|
|
56
|
+
ro.observe(el);
|
|
57
|
+
return () => ro.disconnect();
|
|
58
|
+
}, [cardWidth, aspectRatio, gap]);
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
const el = wrap.current;
|
|
62
|
+
if (!pool) return undefined;
|
|
63
|
+
const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
|
|
64
|
+
const s = { ox: 0, oy: 0, vx: 0, vy: 0, lean: { x: 0, y: 0 }, drag: null, hover: false };
|
|
65
|
+
const slots = tiles.current.slice(0, pool).map(() => ({ key: "" }));
|
|
66
|
+
let raf = 0;
|
|
67
|
+
let last = 0;
|
|
68
|
+
let visible = true;
|
|
69
|
+
|
|
70
|
+
const paint = () => {
|
|
71
|
+
const { cw, ch, cols } = geo.current;
|
|
72
|
+
const items = listRef.current;
|
|
73
|
+
const n = items.length;
|
|
74
|
+
const c0 = Math.floor(-s.ox / cw) - 1;
|
|
75
|
+
const r0 = Math.floor(-s.oy / ch) - 2;
|
|
76
|
+
const rx = Math.max(-10, Math.min(10, -s.lean.y * 0.012 * live.current.tilt));
|
|
77
|
+
const ry = Math.max(-10, Math.min(10, s.lean.x * 0.012 * live.current.tilt));
|
|
78
|
+
const sc = 1 - Math.min(0.06, Math.hypot(s.lean.x, s.lean.y) / 30000);
|
|
79
|
+
for (let k = 0; k < slots.length; k++) {
|
|
80
|
+
const el = tiles.current[k];
|
|
81
|
+
if (!el) continue;
|
|
82
|
+
const i = c0 + (k % cols);
|
|
83
|
+
const j = r0 + Math.floor(k / cols);
|
|
84
|
+
// Odd columns sit half a card lower, like a masonry wall.
|
|
85
|
+
const x = i * cw + s.ox;
|
|
86
|
+
const y = j * ch + s.oy + (((i % 2) + 2) % 2) * ch * 0.5;
|
|
87
|
+
el.style.transform = `translate3d(${x.toFixed(2)}px, ${y.toFixed(2)}px, 0) rotateX(${rx.toFixed(2)}deg) rotateY(${ry.toFixed(2)}deg) scale(${sc.toFixed(4)})`;
|
|
88
|
+
const key = `${i},${j}`;
|
|
89
|
+
if (slots[k].key !== key) {
|
|
90
|
+
slots[k].key = key;
|
|
91
|
+
const idx = ((((i * 5 + j * 3) % n) + n) % n);
|
|
92
|
+
el.dataset.index = String(idx);
|
|
93
|
+
const img = el.querySelector("img");
|
|
94
|
+
if (img && img.getAttribute("src") !== items[idx].image) img.setAttribute("src", items[idx].image);
|
|
95
|
+
const label = el.querySelector("[data-title]");
|
|
96
|
+
if (label) label.textContent = items[idx].title ?? "";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const frame = (now) => {
|
|
102
|
+
raf = 0;
|
|
103
|
+
const o = live.current;
|
|
104
|
+
const dt = last ? Math.min(0.05, (now - last) / 1000) : 1 / 60;
|
|
105
|
+
last = now;
|
|
106
|
+
if (!s.drag) {
|
|
107
|
+
const k = Math.pow(o.friction, dt * 60);
|
|
108
|
+
s.vx *= k;
|
|
109
|
+
s.vy *= k;
|
|
110
|
+
// Ease back into the idle drift once the throw has died down.
|
|
111
|
+
if (!s.hover && !reduced && o.drift) {
|
|
112
|
+
s.vx += (-o.drift * 0.6 - s.vx) * (1 - Math.exp(-dt / 1.6)) * (Math.abs(s.vx) < 120 ? 1 : 0);
|
|
113
|
+
s.vy += (-o.drift - s.vy) * (1 - Math.exp(-dt / 1.6)) * (Math.abs(s.vy) < 120 ? 1 : 0);
|
|
114
|
+
}
|
|
115
|
+
s.ox += s.vx * dt;
|
|
116
|
+
s.oy += s.vy * dt;
|
|
117
|
+
}
|
|
118
|
+
const lx = s.drag ? s.drag.vx : s.vx;
|
|
119
|
+
const ly = s.drag ? s.drag.vy : s.vy;
|
|
120
|
+
s.lean.x += (lx - s.lean.x) * (1 - Math.exp(-dt / 0.12));
|
|
121
|
+
s.lean.y += (ly - s.lean.y) * (1 - Math.exp(-dt / 0.12));
|
|
122
|
+
paint();
|
|
123
|
+
const moving = s.drag || Math.abs(s.vx) + Math.abs(s.vy) > 1 || Math.abs(s.lean.x) + Math.abs(s.lean.y) > 1;
|
|
124
|
+
if (visible && (moving || (o.drift && !s.hover && !reduced))) raf = requestAnimationFrame(frame);
|
|
125
|
+
else last = 0;
|
|
126
|
+
};
|
|
127
|
+
const start = () => {
|
|
128
|
+
if (!raf && visible) raf = requestAnimationFrame(frame);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const onDown = (e) => {
|
|
132
|
+
if (e.button !== 0) return;
|
|
133
|
+
e.preventDefault();
|
|
134
|
+
s.drag = { x: e.clientX, y: e.clientY, ox: s.ox, oy: s.oy, t: performance.now(), vx: 0, vy: 0, moved: false, target: e.target.closest("[data-index]") };
|
|
135
|
+
el.setPointerCapture(e.pointerId);
|
|
136
|
+
s.vx = s.vy = 0;
|
|
137
|
+
start();
|
|
138
|
+
};
|
|
139
|
+
const onMove = (e) => {
|
|
140
|
+
const g = s.drag;
|
|
141
|
+
if (!g) return;
|
|
142
|
+
const now = performance.now();
|
|
143
|
+
const dx = e.clientX - g.x;
|
|
144
|
+
const dy = e.clientY - g.y;
|
|
145
|
+
if (Math.hypot(dx, dy) > 5) g.moved = true;
|
|
146
|
+
const nx = g.ox + dx;
|
|
147
|
+
const ny = g.oy + dy;
|
|
148
|
+
const span = Math.max(8, now - g.t) / 1000;
|
|
149
|
+
g.vx += ((nx - s.ox) / span - g.vx) * 0.4;
|
|
150
|
+
g.vy += ((ny - s.oy) / span - g.vy) * 0.4;
|
|
151
|
+
g.t = now;
|
|
152
|
+
s.ox = nx;
|
|
153
|
+
s.oy = ny;
|
|
154
|
+
start();
|
|
155
|
+
};
|
|
156
|
+
const onUp = (e) => {
|
|
157
|
+
const g = s.drag;
|
|
158
|
+
if (!g) return;
|
|
159
|
+
s.drag = null;
|
|
160
|
+
if (g.moved) {
|
|
161
|
+
// Fling: keep the release speed (capped) and let friction take it.
|
|
162
|
+
s.vx = Math.max(-3000, Math.min(3000, g.vx));
|
|
163
|
+
s.vy = Math.max(-3000, Math.min(3000, g.vy));
|
|
164
|
+
} else if (g.target && e.type === "pointerup") {
|
|
165
|
+
const idx = Number(g.target.dataset.index);
|
|
166
|
+
live.current.onItemClick?.(listRef.current[idx], idx);
|
|
167
|
+
}
|
|
168
|
+
start();
|
|
169
|
+
};
|
|
170
|
+
const onWheel = (e) => {
|
|
171
|
+
e.preventDefault();
|
|
172
|
+
s.vx = -e.deltaX * 6;
|
|
173
|
+
s.vy = -e.deltaY * 6;
|
|
174
|
+
start();
|
|
175
|
+
};
|
|
176
|
+
const onEnter = () => {
|
|
177
|
+
s.hover = true;
|
|
178
|
+
};
|
|
179
|
+
const onLeave = () => {
|
|
180
|
+
s.hover = false;
|
|
181
|
+
start();
|
|
182
|
+
};
|
|
183
|
+
el.addEventListener("pointerdown", onDown);
|
|
184
|
+
el.addEventListener("pointermove", onMove);
|
|
185
|
+
el.addEventListener("pointerup", onUp);
|
|
186
|
+
el.addEventListener("pointercancel", onUp);
|
|
187
|
+
el.addEventListener("pointerenter", onEnter);
|
|
188
|
+
el.addEventListener("pointerleave", onLeave);
|
|
189
|
+
el.addEventListener("wheel", onWheel, { passive: false });
|
|
190
|
+
const io = new IntersectionObserver(([entry]) => {
|
|
191
|
+
visible = entry.isIntersecting;
|
|
192
|
+
if (visible) start();
|
|
193
|
+
});
|
|
194
|
+
io.observe(el);
|
|
195
|
+
paint();
|
|
196
|
+
start();
|
|
197
|
+
|
|
198
|
+
return () => {
|
|
199
|
+
cancelAnimationFrame(raf);
|
|
200
|
+
io.disconnect();
|
|
201
|
+
el.removeEventListener("pointerdown", onDown);
|
|
202
|
+
el.removeEventListener("pointermove", onMove);
|
|
203
|
+
el.removeEventListener("pointerup", onUp);
|
|
204
|
+
el.removeEventListener("pointercancel", onUp);
|
|
205
|
+
el.removeEventListener("pointerenter", onEnter);
|
|
206
|
+
el.removeEventListener("pointerleave", onLeave);
|
|
207
|
+
el.removeEventListener("wheel", onWheel);
|
|
208
|
+
};
|
|
209
|
+
}, [pool]);
|
|
210
|
+
|
|
211
|
+
const { w, h } = geo.current;
|
|
212
|
+
return (
|
|
213
|
+
<div
|
|
214
|
+
ref={wrap}
|
|
215
|
+
className={`relative w-full cursor-grab touch-none select-none overflow-hidden active:cursor-grabbing ${className}`}
|
|
216
|
+
style={{ height, perspective: "1200px", maskImage: "radial-gradient(ellipse 75% 75% at 50% 50%, #000 55%, transparent 100%)", WebkitMaskImage: "radial-gradient(ellipse 75% 75% at 50% 50%, #000 55%, transparent 100%)" }}
|
|
217
|
+
>
|
|
218
|
+
{Array.from({ length: pool }, (_, k) => (
|
|
219
|
+
<div key={k} ref={(e) => (tiles.current[k] = e)} className="group absolute left-0 top-0 will-change-transform" style={{ width: w, height: h }}>
|
|
220
|
+
<div className="h-full w-full overflow-hidden rounded-2xl bg-white/5 shadow-[0_18px_40px_-22px_rgba(0,0,0,0.7)] ring-1 ring-white/10">
|
|
221
|
+
<img alt="" draggable={false} loading="lazy" decoding="async" className="h-full w-full object-cover transition-transform duration-500 ease-out group-hover:scale-[1.06]" />
|
|
222
|
+
</div>
|
|
223
|
+
{showTitles && <p data-title="" className="pointer-events-none absolute inset-x-3 bottom-3 truncate rounded-full bg-black/45 px-3 py-1 text-xs font-semibold text-white opacity-0 backdrop-blur transition-opacity duration-300 group-hover:opacity-100" />}
|
|
224
|
+
</div>
|
|
225
|
+
))}
|
|
226
|
+
</div>
|
|
227
|
+
);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
export default InfiniteCanvas;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import React, { useRef } from "react";
|
|
2
|
+
import { motion, useMotionValue, useReducedMotion, useSpring, useTransform } from "framer-motion";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Magnetic Button: when the pointer comes near, the button leans toward it
|
|
6
|
+
* like a magnet, and its label follows a little further for a layered
|
|
7
|
+
* parallax. A soft glow tracks the pointer across the surface. Springs back
|
|
8
|
+
* when the pointer leaves. Renders a <button>, or a link with `as="a"`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const MagneticButton = ({ as = "button", children = "Get started", strength = 0.35, radius = 1.6, accentColor = "#a3e635", theme = "dark", className = "", ...rest }) => {
|
|
12
|
+
const reduced = useReducedMotion();
|
|
13
|
+
const ref = useRef(null);
|
|
14
|
+
const mx = useMotionValue(0);
|
|
15
|
+
const my = useMotionValue(0);
|
|
16
|
+
const gx = useMotionValue(50);
|
|
17
|
+
const gy = useMotionValue(50);
|
|
18
|
+
const spring = { stiffness: 220, damping: 18, mass: 0.6 };
|
|
19
|
+
const x = useSpring(mx, spring);
|
|
20
|
+
const y = useSpring(my, spring);
|
|
21
|
+
// The label travels a bit further than the button: layered parallax.
|
|
22
|
+
const lx = useTransform(x, (v) => v * 0.45);
|
|
23
|
+
const ly = useTransform(y, (v) => v * 0.45);
|
|
24
|
+
const glow = useTransform([gx, gy], ([a, b]) => `radial-gradient(120px circle at ${a}% ${b}%, ${accentColor}55, transparent 70%)`);
|
|
25
|
+
const Tag = motion[as] ?? motion.button;
|
|
26
|
+
const dark = theme !== "light";
|
|
27
|
+
|
|
28
|
+
const onMove = (e) => {
|
|
29
|
+
const el = ref.current;
|
|
30
|
+
if (!el || reduced) return;
|
|
31
|
+
const r = el.getBoundingClientRect();
|
|
32
|
+
const cx = r.left + r.width / 2;
|
|
33
|
+
const cy = r.top + r.height / 2;
|
|
34
|
+
const dx = e.clientX - cx;
|
|
35
|
+
const dy = e.clientY - cy;
|
|
36
|
+
// Only pull while the pointer is within `radius` × the button size.
|
|
37
|
+
const reach = Math.max(r.width, r.height) * radius;
|
|
38
|
+
const pull = Math.max(0, 1 - Math.hypot(dx, dy) / reach);
|
|
39
|
+
mx.set(dx * strength * pull * 1.6);
|
|
40
|
+
my.set(dy * strength * pull * 1.6);
|
|
41
|
+
gx.set(((e.clientX - r.left) / r.width) * 100);
|
|
42
|
+
gy.set(((e.clientY - r.top) / r.height) * 100);
|
|
43
|
+
};
|
|
44
|
+
const reset = () => {
|
|
45
|
+
mx.set(0);
|
|
46
|
+
my.set(0);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
// The wrapper is larger than the button so the pull starts before you touch it.
|
|
51
|
+
<span className="inline-block p-6" onPointerMove={onMove} onPointerLeave={reset}>
|
|
52
|
+
<Tag
|
|
53
|
+
ref={ref}
|
|
54
|
+
{...(as === "button" ? { type: "button" } : {})}
|
|
55
|
+
{...rest}
|
|
56
|
+
style={{ x, y }}
|
|
57
|
+
whileTap={{ scale: 0.96 }}
|
|
58
|
+
className={`group relative inline-flex select-none items-center justify-center overflow-hidden rounded-full px-8 py-4 text-[0.95rem] font-semibold outline-none ring-1 transition-shadow focus-visible:ring-2 ${dark ? "bg-neutral-950 text-white ring-white/15 focus-visible:ring-white/40" : "bg-white text-neutral-900 ring-black/10 focus-visible:ring-black/30"} ${className}`}
|
|
59
|
+
>
|
|
60
|
+
<motion.span aria-hidden="true" className="pointer-events-none absolute inset-0 opacity-0 transition-opacity duration-300 group-hover:opacity-100" style={{ background: glow }} />
|
|
61
|
+
<motion.span className="relative inline-flex items-center gap-2" style={{ x: lx, y: ly }}>
|
|
62
|
+
{children}
|
|
63
|
+
</motion.span>
|
|
64
|
+
</Tag>
|
|
65
|
+
</span>
|
|
66
|
+
);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export default MagneticButton;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { motion, useAnimationFrame, useMotionValue, useReducedMotion, useTransform } from "framer-motion";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Marquee 3D: a wall of cards tilted back in 3D, with columns that scroll in
|
|
6
|
+
* opposite directions forever. Great for showing off screenshots, logos or
|
|
7
|
+
* testimonials. Each column repeats its items twice and moves exactly one
|
|
8
|
+
* copy, so the loop has no seam. Hover pauses; edges fade out.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const IK = "https://ik.imagekit.io/ny6yinyut/apexui/assets/componentsimage";
|
|
12
|
+
const shot = (f) => `${IK}/${f}?tr=f-auto,q-75,w-420`;
|
|
13
|
+
const DEFAULT_IMAGES = [
|
|
14
|
+
"gooey-nav-v3.png", "liquid-blob-3d.png", "galaxy-3d.png", "warp-tunnel.png", "retro-grid-3d.png", "particle-morph.png",
|
|
15
|
+
"distortion-image.png", "cube-gallery-v2.png", "coverflow-3d.png", "ai-voice-orb.png", "flip-book-v2.png", "holo-card.png",
|
|
16
|
+
"dot-globe.png", "3d-carousel.png", "pricing-cards.png", "stepper.png",
|
|
17
|
+
].map(shot);
|
|
18
|
+
|
|
19
|
+
function Column({ images, duration, reverse, paused }) {
|
|
20
|
+
const reduced = useReducedMotion();
|
|
21
|
+
const list = [...images, ...images];
|
|
22
|
+
// Progress through one copy (0 to 50%), advanced every frame. Pausing just
|
|
23
|
+
// stops advancing, so hover-out continues from the same spot (no jump).
|
|
24
|
+
const offset = useMotionValue(0);
|
|
25
|
+
useAnimationFrame((_, delta) => {
|
|
26
|
+
if (paused || reduced) return;
|
|
27
|
+
offset.set((offset.get() + (Math.min(delta, 50) / 1000 / duration) * 50) % 50);
|
|
28
|
+
});
|
|
29
|
+
const y = useTransform(offset, (v) => `${reverse ? v - 50 : -v}%`);
|
|
30
|
+
return (
|
|
31
|
+
<div className="relative h-full overflow-hidden">
|
|
32
|
+
<motion.div className="flex flex-col gap-4" style={{ y }}>
|
|
33
|
+
{list.map((src, i) => (
|
|
34
|
+
<img key={i} src={src} alt="" draggable="false" loading="lazy" className="aspect-[4/3] w-full rounded-xl bg-neutral-900 object-cover shadow-[0_10px_30px_-12px_rgba(0,0,0,0.6)] ring-1 ring-white/10" />
|
|
35
|
+
))}
|
|
36
|
+
</motion.div>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const Marquee3D = ({ images = DEFAULT_IMAGES, columns = 4, speed = 1, tilt = 1, pauseOnHover = true, height = "100%", className = "" }) => {
|
|
42
|
+
const [hover, setHover] = React.useState(false);
|
|
43
|
+
const cols = Array.from({ length: columns }, (_, c) => images.filter((_, i) => i % columns === c));
|
|
44
|
+
return (
|
|
45
|
+
<div
|
|
46
|
+
className={`relative w-full overflow-hidden [perspective:1000px] ${className}`}
|
|
47
|
+
style={{ height, maskImage: "linear-gradient(to bottom, transparent, black 18%, black 82%, transparent)", WebkitMaskImage: "linear-gradient(to bottom, transparent, black 18%, black 82%, transparent)" }}
|
|
48
|
+
onPointerEnter={() => pauseOnHover && setHover(true)}
|
|
49
|
+
onPointerLeave={() => setHover(false)}
|
|
50
|
+
aria-hidden="true"
|
|
51
|
+
>
|
|
52
|
+
<div
|
|
53
|
+
className="absolute left-1/2 top-1/2 grid h-[170%] w-[115%] gap-4"
|
|
54
|
+
style={{
|
|
55
|
+
gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))`,
|
|
56
|
+
transform: `translate(-50%, -50%) rotateX(${32 * tilt}deg) rotateZ(${-22 * tilt}deg) rotateY(${8 * tilt}deg)`,
|
|
57
|
+
transformStyle: "preserve-3d",
|
|
58
|
+
}}
|
|
59
|
+
>
|
|
60
|
+
{cols.map((imgs, c) => (
|
|
61
|
+
<Column key={c} images={imgs.length ? imgs : images} duration={(28 + c * 6) / speed} reverse={c % 2 === 1} paused={hover} />
|
|
62
|
+
))}
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export default Marquee3D;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from 'react';
|
|
2
2
|
import { AnimatePresence, LayoutGroup, MotionConfig, motion } from 'framer-motion';
|
|
3
|
-
import { ArrowRight, ArrowUpRight, ChevronDown } from 'lucide-react';
|
|
3
|
+
import { ArrowRight, ArrowUpRight, ChevronDown, Menu, X } from 'lucide-react';
|
|
4
4
|
|
|
5
5
|
const ease = [0.22, 1, 0.36, 1];
|
|
6
6
|
const CLOSE_DELAY = 140;
|
|
@@ -70,7 +70,129 @@ const PanelContent = ({ item }) => {
|
|
|
70
70
|
return <Services items={item.submenu} />;
|
|
71
71
|
};
|
|
72
72
|
|
|
73
|
+
/*
|
|
74
|
+
* Phones: a Menu button opens one full-width sheet; each section expands in
|
|
75
|
+
* place (accordion) instead of a hover panel that would not fit.
|
|
76
|
+
*/
|
|
77
|
+
const MobileNav = ({ navData }) => {
|
|
78
|
+
const [open, setOpen] = useState(false);
|
|
79
|
+
const [section, setSection] = useState(null);
|
|
80
|
+
const root = useRef(null);
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (!open) return undefined;
|
|
83
|
+
const down = (e) => !root.current?.contains(e.target) && setOpen(false);
|
|
84
|
+
const key = (e) => e.key === 'Escape' && setOpen(false);
|
|
85
|
+
window.addEventListener('pointerdown', down);
|
|
86
|
+
window.addEventListener('keydown', key);
|
|
87
|
+
return () => {
|
|
88
|
+
window.removeEventListener('pointerdown', down);
|
|
89
|
+
window.removeEventListener('keydown', key);
|
|
90
|
+
};
|
|
91
|
+
}, [open]);
|
|
92
|
+
return (
|
|
93
|
+
<div ref={root} className="relative w-full max-w-sm">
|
|
94
|
+
<button
|
|
95
|
+
type="button"
|
|
96
|
+
aria-expanded={open}
|
|
97
|
+
onClick={() => setOpen((o) => !o)}
|
|
98
|
+
className="flex w-full items-center justify-between rounded-full border border-white/10 bg-zinc-950/85 px-5 py-3 text-sm font-medium text-zinc-200 shadow-[0_10px_40px_-12px_rgba(0,0,0,0.7)] outline-none backdrop-blur-xl focus-visible:ring-2 focus-visible:ring-lime-400/60"
|
|
99
|
+
>
|
|
100
|
+
Menu
|
|
101
|
+
{open ? <X size={18} /> : <Menu size={18} />}
|
|
102
|
+
</button>
|
|
103
|
+
<AnimatePresence>
|
|
104
|
+
{open && (
|
|
105
|
+
<motion.nav
|
|
106
|
+
aria-label="Main"
|
|
107
|
+
initial={{ opacity: 0, y: -8, scale: 0.98 }}
|
|
108
|
+
animate={{ opacity: 1, y: 0, scale: 1 }}
|
|
109
|
+
exit={{ opacity: 0, y: -6, scale: 0.98, transition: { duration: 0.15 } }}
|
|
110
|
+
transition={{ duration: 0.22, ease }}
|
|
111
|
+
className="absolute left-0 right-0 top-full z-10 mt-2 max-h-[70vh] origin-top overflow-y-auto rounded-2xl border border-white/10 bg-zinc-950/95 p-2 shadow-[0_24px_60px_-20px_rgba(0,0,0,0.8)] backdrop-blur-xl"
|
|
112
|
+
>
|
|
113
|
+
{navData.map((item, i) => {
|
|
114
|
+
const hasMenu = Boolean(item.submenu?.length);
|
|
115
|
+
const isOpen = section === i;
|
|
116
|
+
if (!hasMenu) {
|
|
117
|
+
return (
|
|
118
|
+
<a key={i} href={item.href || '#'} className="block rounded-xl px-3 py-3 text-sm font-medium text-zinc-200 outline-none hover:bg-white/[0.05] focus-visible:bg-white/[0.07]">
|
|
119
|
+
{item.label}
|
|
120
|
+
</a>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
return (
|
|
124
|
+
<div key={i}>
|
|
125
|
+
<button
|
|
126
|
+
type="button"
|
|
127
|
+
aria-expanded={isOpen}
|
|
128
|
+
onClick={() => setSection(isOpen ? null : i)}
|
|
129
|
+
className={`flex w-full items-center justify-between rounded-xl px-3 py-3 text-sm font-medium outline-none hover:bg-white/[0.05] focus-visible:bg-white/[0.07] ${isOpen ? 'text-lime-300' : 'text-zinc-200'}`}
|
|
130
|
+
>
|
|
131
|
+
{item.label}
|
|
132
|
+
<ChevronDown size={16} className={`transition-transform duration-300 ${isOpen ? 'rotate-180' : ''}`} />
|
|
133
|
+
</button>
|
|
134
|
+
<AnimatePresence initial={false}>
|
|
135
|
+
{isOpen && (
|
|
136
|
+
<motion.ul
|
|
137
|
+
initial={{ height: 0, opacity: 0 }}
|
|
138
|
+
animate={{ height: 'auto', opacity: 1 }}
|
|
139
|
+
exit={{ height: 0, opacity: 0 }}
|
|
140
|
+
transition={{ duration: 0.25, ease }}
|
|
141
|
+
className="overflow-hidden pl-2"
|
|
142
|
+
>
|
|
143
|
+
{item.submenu.map((sub, j) => (
|
|
144
|
+
<li key={j}>
|
|
145
|
+
<a href={sub.href} className="flex items-center gap-3 rounded-lg px-3 py-2 outline-none hover:bg-white/[0.05] focus-visible:bg-white/[0.07]">
|
|
146
|
+
{sub.image ? (
|
|
147
|
+
<img src={sub.image} alt="" className="h-9 w-9 shrink-0 rounded-md object-cover ring-1 ring-white/10" />
|
|
148
|
+
) : (
|
|
149
|
+
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg border border-lime-400/20 bg-lime-400/10 text-xs font-semibold text-lime-300">
|
|
150
|
+
{sub.icon ? <sub.icon size={14} /> : sub.label.charAt(0)}
|
|
151
|
+
</span>
|
|
152
|
+
)}
|
|
153
|
+
<span className="min-w-0">
|
|
154
|
+
<span className="block truncate text-sm text-zinc-100">{sub.label}</span>
|
|
155
|
+
{sub.subtitle && <span className="block truncate text-xs text-zinc-500">{sub.subtitle}</span>}
|
|
156
|
+
</span>
|
|
157
|
+
</a>
|
|
158
|
+
</li>
|
|
159
|
+
))}
|
|
160
|
+
</motion.ul>
|
|
161
|
+
)}
|
|
162
|
+
</AnimatePresence>
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
})}
|
|
166
|
+
</motion.nav>
|
|
167
|
+
)}
|
|
168
|
+
</AnimatePresence>
|
|
169
|
+
</div>
|
|
170
|
+
);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Below this much room the desktop bar would not fit, so the phone menu shows.
|
|
174
|
+
const COMPACT_BELOW = 640;
|
|
175
|
+
|
|
73
176
|
const NavDropDown = ({ navData = [], className = '' }) => {
|
|
177
|
+
const hostRef = useRef(null);
|
|
178
|
+
const [compact, setCompact] = useState(false);
|
|
179
|
+
useLayoutEffect(() => {
|
|
180
|
+
const host = hostRef.current?.parentElement;
|
|
181
|
+
if (!host) return undefined;
|
|
182
|
+
const check = () => setCompact(host.clientWidth < COMPACT_BELOW);
|
|
183
|
+
check();
|
|
184
|
+
const ro = new ResizeObserver(check);
|
|
185
|
+
ro.observe(host);
|
|
186
|
+
return () => ro.disconnect();
|
|
187
|
+
}, []);
|
|
188
|
+
return (
|
|
189
|
+
<div ref={hostRef} className={`flex w-full justify-center ${className}`}>
|
|
190
|
+
{compact ? <MobileNav navData={navData} /> : <DesktopNav navData={navData} />}
|
|
191
|
+
</div>
|
|
192
|
+
);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const DesktopNav = ({ navData = [], className = '' }) => {
|
|
74
196
|
const uid = useId();
|
|
75
197
|
const rootRef = useRef(null);
|
|
76
198
|
const barRef = useRef(null);
|