apex-ui-kit 2.9.0 → 2.11.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 +7 -4
- package/dist/ApexUI-Kit/templates/AuroraBackground/AuroraBackground.jsx +157 -35
- package/dist/ApexUI-Kit/templates/BluePrintBackground/BluePrintBackground.jsx +134 -145
- package/dist/ApexUI-Kit/templates/CardSwap3D/CardSwap3D.jsx +64 -40
- package/dist/ApexUI-Kit/templates/DotGlobe/DotGlobe.jsx +6 -3
- package/dist/ApexUI-Kit/templates/FluidCursor/FluidCursor.jsx +418 -0
- package/dist/ApexUI-Kit/templates/Folder/Folder.jsx +58 -36
- package/dist/ApexUI-Kit/templates/ImageAccordion/ImageAccordion.jsx +8 -6
- package/dist/ApexUI-Kit/templates/InfiniteCanvas/InfiniteCanvas.jsx +1 -1
- package/dist/ApexUI-Kit/templates/IsoStack/IsoStack.jsx +28 -10
- package/dist/ApexUI-Kit/templates/LuminousParticleOcean/LuminousParticleOcean.jsx +8 -6
- package/dist/ApexUI-Kit/templates/MeteorCard/MeteorCard.jsx +5 -5
- package/dist/ApexUI-Kit/templates/OrbitIcons/OrbitIcons.jsx +1 -1
- package/dist/ApexUI-Kit/templates/ParallaxCard/ParallaxCard.jsx +34 -33
- package/dist/ApexUI-Kit/templates/SketchReveal/SketchReveal.jsx +257 -0
- package/dist/ApexUI-Kit/templates/SmokeBackground/SmokeBackground.jsx +237 -235
- package/dist/cli/index.js +42 -20
- package/package.json +8 -2
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import React, { useState } from "react";
|
|
2
|
-
import { motion } from "framer-motion";
|
|
1
|
+
import React, { useLayoutEffect, useRef, useState } from "react";
|
|
2
|
+
import { motion, useMotionValue, useSpring } from "framer-motion";
|
|
3
3
|
|
|
4
4
|
/*
|
|
5
5
|
* Folder: a little folder that opens on click — the front flap tilts down
|
|
6
6
|
* and up to three papers fan out of it. While open, each paper leans toward
|
|
7
|
-
* the pointer a little. Click again to tuck everything back in.
|
|
7
|
+
* the pointer a little. Click again to tuck everything back in. It shrinks
|
|
8
|
+
* to fit narrow containers so the open fan never gets cut off.
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
const DEFAULT_ITEMS = [
|
|
@@ -32,23 +33,66 @@ const SPOTS = [
|
|
|
32
33
|
{ x: 0, y: -4.2, r: 3 },
|
|
33
34
|
{ x: 3.9, y: -3.3, r: 15 },
|
|
34
35
|
];
|
|
36
|
+
// Width of the open fan in em (papers plus their spread), used to fit the container.
|
|
37
|
+
const FAN_WIDTH = 15.5;
|
|
38
|
+
|
|
39
|
+
// One paper. Its lean follows the pointer through motion values, so moving the
|
|
40
|
+
// mouse never re-renders the folder.
|
|
41
|
+
function Paper({ children, spot, index, open }) {
|
|
42
|
+
const lx = useMotionValue(0);
|
|
43
|
+
const ly = useMotionValue(0);
|
|
44
|
+
const x = useSpring(lx, { stiffness: 260, damping: 20 });
|
|
45
|
+
const y = useSpring(ly, { stiffness: 260, damping: 20 });
|
|
46
|
+
return (
|
|
47
|
+
<motion.span
|
|
48
|
+
className="absolute bottom-[10%] left-[10%] h-[80%] w-[80%]"
|
|
49
|
+
initial={false}
|
|
50
|
+
animate={open ? { x: `${spot.x}em`, y: `${spot.y}em`, rotate: spot.r, scale: 1 } : { x: "0em", y: `${-0.3 - index * 0.2}em`, rotate: 0, scale: 0.92 - (2 - index) * 0.03 }}
|
|
51
|
+
transition={{ type: "spring", stiffness: 300, damping: 22, delay: open ? index * 0.04 : 0 }}
|
|
52
|
+
onPointerMove={(e) => {
|
|
53
|
+
if (!open) return;
|
|
54
|
+
const r = e.currentTarget.getBoundingClientRect();
|
|
55
|
+
lx.set(((e.clientX - (r.left + r.width / 2)) / r.width) * 10);
|
|
56
|
+
ly.set(((e.clientY - (r.top + r.height / 2)) / r.height) * 10);
|
|
57
|
+
}}
|
|
58
|
+
onPointerLeave={() => {
|
|
59
|
+
lx.set(0);
|
|
60
|
+
ly.set(0);
|
|
61
|
+
}}
|
|
62
|
+
>
|
|
63
|
+
<motion.span className="block h-full w-full overflow-hidden rounded-[0.4em] bg-white text-left shadow-md" style={{ x, y }}>
|
|
64
|
+
{children}
|
|
65
|
+
</motion.span>
|
|
66
|
+
</motion.span>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
35
69
|
|
|
36
70
|
const Folder = ({ color = "#a3e635", size = 1, items = DEFAULT_ITEMS, label = "Projects", className = "" }) => {
|
|
37
71
|
const [open, setOpen] = useState(false);
|
|
38
|
-
const [
|
|
72
|
+
const [fit, setFit] = useState(size);
|
|
73
|
+
const root = useRef(null);
|
|
39
74
|
const papers = items.slice(0, 3);
|
|
40
75
|
const back = `color-mix(in srgb, ${color} 75%, #000)`;
|
|
41
76
|
|
|
77
|
+
// Shrink when the container can't hold the open fan.
|
|
78
|
+
useLayoutEffect(() => {
|
|
79
|
+
const el = root.current?.parentElement;
|
|
80
|
+
if (!el) return undefined;
|
|
81
|
+
const ro = new ResizeObserver(() => {
|
|
82
|
+
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
|
|
83
|
+
setFit(Math.min(size, el.clientWidth / (FAN_WIDTH * rem)));
|
|
84
|
+
});
|
|
85
|
+
ro.observe(el);
|
|
86
|
+
return () => ro.disconnect();
|
|
87
|
+
}, [size]);
|
|
88
|
+
|
|
42
89
|
return (
|
|
43
|
-
<div className={`flex flex-col items-center gap-4 ${className}`} style={{ fontSize: `${
|
|
90
|
+
<div ref={root} className={`flex flex-col items-center gap-4 pt-[4.5em] ${className}`} style={{ fontSize: `${fit}rem` }}>
|
|
44
91
|
<button
|
|
45
92
|
type="button"
|
|
46
93
|
aria-expanded={open}
|
|
47
94
|
aria-label={`${label} folder`}
|
|
48
|
-
onClick={() =>
|
|
49
|
-
setOpen((o) => !o);
|
|
50
|
-
setLean([]);
|
|
51
|
-
}}
|
|
95
|
+
onClick={() => setOpen((o) => !o)}
|
|
52
96
|
className="group relative h-[5.5em] w-[7em] outline-none transition-transform duration-200 hover:-translate-y-1"
|
|
53
97
|
style={{ perspective: "600px" }}
|
|
54
98
|
>
|
|
@@ -56,33 +100,11 @@ const Folder = ({ color = "#a3e635", size = 1, items = DEFAULT_ITEMS, label = "P
|
|
|
56
100
|
<span className="absolute inset-0 rounded-[0.6em] rounded-tl-none" style={{ background: back }} />
|
|
57
101
|
<span className="absolute -top-[0.7em] left-0 h-[0.9em] w-[2.6em] rounded-t-[0.45em]" style={{ background: back }} />
|
|
58
102
|
|
|
59
|
-
{papers.map((p, i) =>
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
key={i}
|
|
65
|
-
className="absolute bottom-[10%] left-[10%] h-[80%] w-[80%] overflow-hidden rounded-[0.4em] bg-white text-left shadow-md"
|
|
66
|
-
initial={false}
|
|
67
|
-
animate={open ? { x: `${spot.x + l.x}em`, y: `${spot.y + l.y}em`, rotate: spot.r, scale: 1 } : { x: "0em", y: `${-0.3 - i * 0.2}em`, rotate: 0, scale: 0.92 - (2 - i) * 0.03 }}
|
|
68
|
-
transition={{ type: "spring", stiffness: 300, damping: 22, delay: open ? i * 0.04 : 0 }}
|
|
69
|
-
onPointerMove={(e) => {
|
|
70
|
-
if (!open) return;
|
|
71
|
-
const r = e.currentTarget.getBoundingClientRect();
|
|
72
|
-
const next = [...lean];
|
|
73
|
-
next[i] = { x: ((e.clientX - (r.left + r.width / 2)) / r.width) * 0.8, y: ((e.clientY - (r.top + r.height / 2)) / r.height) * 0.8 };
|
|
74
|
-
setLean(next);
|
|
75
|
-
}}
|
|
76
|
-
onPointerLeave={() => {
|
|
77
|
-
const next = [...lean];
|
|
78
|
-
next[i] = { x: 0, y: 0 };
|
|
79
|
-
setLean(next);
|
|
80
|
-
}}
|
|
81
|
-
>
|
|
82
|
-
{p}
|
|
83
|
-
</motion.span>
|
|
84
|
-
);
|
|
85
|
-
})}
|
|
103
|
+
{papers.map((p, i) => (
|
|
104
|
+
<Paper key={i} index={i} open={open} spot={SPOTS[papers.length === 1 ? 1 : papers.length === 2 ? i * 2 : i]}>
|
|
105
|
+
{p}
|
|
106
|
+
</Paper>
|
|
107
|
+
))}
|
|
86
108
|
|
|
87
109
|
{/* Front flap */}
|
|
88
110
|
<motion.span
|
|
@@ -80,14 +80,16 @@ const ImageAccordion = ({ items = DEFAULT_ITEMS, defaultActive = 0, autoPlay = 0
|
|
|
80
80
|
alt=""
|
|
81
81
|
draggable={false}
|
|
82
82
|
className="absolute inset-0 h-full w-full object-cover"
|
|
83
|
-
style={{ transform: on ? "scale(1)" : "scale(1.12)",
|
|
83
|
+
style={{ transform: on ? "scale(1)" : "scale(1.12)", transition: `transform ${duration * 1.4}ms ${EASE}`, willChange: "transform" }}
|
|
84
84
|
/>
|
|
85
|
+
{/* Dim folded panels with an overlay (cheap) instead of a filter on the image (repaints every frame). */}
|
|
86
|
+
<span aria-hidden="true" className="absolute inset-0 bg-black" style={{ opacity: on ? 0 : 0.32, transition: `opacity ${duration}ms ${EASE}` }} />
|
|
85
87
|
<span aria-hidden="true" className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent" />
|
|
86
88
|
|
|
87
89
|
{/* Folded: the title runs along the strip. */}
|
|
88
90
|
<span
|
|
89
91
|
aria-hidden="true"
|
|
90
|
-
className="absolute whitespace-nowrap text-sm font-semibold tracking-wide"
|
|
92
|
+
className="absolute whitespace-nowrap text-sm font-semibold tracking-wide [backface-visibility:hidden]"
|
|
91
93
|
style={{
|
|
92
94
|
...(vertical ? { left: "1rem", top: "50%", transform: "translateY(-50%)" } : { left: "50%", bottom: "1.25rem", transform: "translateX(-50%) rotate(-90deg)", transformOrigin: "center", translate: "0 -2.5rem" }),
|
|
93
95
|
opacity: on ? 0 : 0.9,
|
|
@@ -97,14 +99,14 @@ const ImageAccordion = ({ items = DEFAULT_ITEMS, defaultActive = 0, autoPlay = 0
|
|
|
97
99
|
{item.title}
|
|
98
100
|
</span>
|
|
99
101
|
|
|
100
|
-
{/* Open: title and text rise in once the panel has room. */}
|
|
102
|
+
{/* Open: title and text rise in once the panel has room. A fixed width keeps them from re-wrapping as it grows. */}
|
|
101
103
|
<span
|
|
102
|
-
className="absolute
|
|
104
|
+
className="absolute bottom-0 left-0 block w-[min(26rem,85vw)] p-[clamp(1rem,3cqw,1.75rem)] [backface-visibility:hidden]"
|
|
103
105
|
style={{ opacity: on ? 1 : 0, transform: on ? "none" : "translateY(0.75rem)", transition: `opacity ${duration * 0.6}ms ${EASE} ${on ? duration * 0.35 : 0}ms, transform ${duration * 0.8}ms ${EASE} ${on ? duration * 0.3 : 0}ms` }}
|
|
104
106
|
>
|
|
105
107
|
<span className="block font-mono text-[0.7rem] tracking-[0.2em] text-white/60">{String(i + 1).padStart(2, "0")}</span>
|
|
106
|
-
<span className="mt-1 block text-[
|
|
107
|
-
{item.description && <span className="mt-1 block max-w-
|
|
108
|
+
<span className="mt-1 block truncate text-[1.6rem] font-bold leading-tight">{item.title}</span>
|
|
109
|
+
{item.description && <span className="mt-1 block max-w-[20rem] text-sm text-white/75">{item.description}</span>}
|
|
108
110
|
</span>
|
|
109
111
|
</button>
|
|
110
112
|
);
|
|
@@ -217,7 +217,7 @@ const InfiniteCanvas = ({ items = DEFAULT_ITEMS, cardWidth = 14, aspectRatio = 0
|
|
|
217
217
|
>
|
|
218
218
|
{Array.from({ length: pool }, (_, k) => (
|
|
219
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
|
|
220
|
+
<div className="h-full w-full overflow-hidden rounded-2xl bg-white/5 ring-1 ring-white/10">
|
|
221
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
222
|
</div>
|
|
223
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" />}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import React, { useState } from "react";
|
|
1
|
+
import React, { useLayoutEffect, useRef, useState } from "react";
|
|
2
2
|
import { motion } from "framer-motion";
|
|
3
3
|
import { Cloud, Database, Layout, Server, Shield } from "lucide-react";
|
|
4
4
|
|
|
5
5
|
/*
|
|
6
6
|
* Iso Stack: layers drawn in isometric 3D, stacked like a sandwich. Hover
|
|
7
7
|
* (or tap) and the stack fans open so each layer's label shows; hover one
|
|
8
|
-
* layer to lift it out. Great for architecture or feature diagrams.
|
|
8
|
+
* layer to lift it out. Great for architecture or feature diagrams. The stack
|
|
9
|
+
* scales to fit narrow containers, and labels move below it on phones.
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
const DEFAULT_LAYERS = [
|
|
@@ -19,13 +20,30 @@ const DEFAULT_LAYERS = [
|
|
|
19
20
|
const IsoStack = ({ layers = DEFAULT_LAYERS, size = 13, gap = 1.6, theme = "dark", className = "" }) => {
|
|
20
21
|
const [open, setOpen] = useState(false);
|
|
21
22
|
const [hover, setHover] = useState(-1);
|
|
23
|
+
const [fit, setFit] = useState(1);
|
|
24
|
+
const [narrow, setNarrow] = useState(false);
|
|
25
|
+
const root = useRef(null);
|
|
22
26
|
const dark = theme !== "light";
|
|
23
27
|
const n = layers.length;
|
|
24
28
|
|
|
29
|
+
// The rotated stack is about 1.45x its size wide; scale it down to fit.
|
|
30
|
+
useLayoutEffect(() => {
|
|
31
|
+
const el = root.current;
|
|
32
|
+
const ro = new ResizeObserver(() => {
|
|
33
|
+
const rem = parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
|
|
34
|
+
const w = el.clientWidth;
|
|
35
|
+
setNarrow(w < 560);
|
|
36
|
+
setFit(Math.min(1, (w - (w < 560 ? 16 : 200)) / (size * 1.45 * rem)));
|
|
37
|
+
});
|
|
38
|
+
ro.observe(el);
|
|
39
|
+
return () => ro.disconnect();
|
|
40
|
+
}, [size]);
|
|
41
|
+
|
|
25
42
|
return (
|
|
26
43
|
<div
|
|
27
|
-
|
|
28
|
-
|
|
44
|
+
ref={root}
|
|
45
|
+
className={`relative flex flex-col items-center justify-center ${className}`}
|
|
46
|
+
style={{ minHeight: `${(size * 0.6 + n * gap * 1.6 + 6) * fit}rem`, width: "100%" }}
|
|
29
47
|
onPointerEnter={(e) => e.pointerType === "mouse" && setOpen(true)}
|
|
30
48
|
onPointerLeave={() => {
|
|
31
49
|
setOpen(false);
|
|
@@ -34,7 +52,7 @@ const IsoStack = ({ layers = DEFAULT_LAYERS, size = 13, gap = 1.6, theme = "dark
|
|
|
34
52
|
onClick={() => setOpen((o) => !o)}
|
|
35
53
|
>
|
|
36
54
|
{/* Shifted down so the fully opened stack stays inside the frame. */}
|
|
37
|
-
<div className="relative" style={{ width: `${size}rem`, height: `${size}rem`, marginTop: `${n * gap * 0.9}rem`, transform:
|
|
55
|
+
<div className="relative" style={{ width: `${size}rem`, height: `${size}rem`, marginTop: `${n * gap * 0.9 * fit}rem`, marginBottom: `${-size * (1 - fit) * 0.6}rem`, transform: `scale(${fit}) rotateX(58deg) rotateZ(-45deg)`, transformStyle: "preserve-3d" }}>
|
|
38
56
|
{layers.map((layer, i) => {
|
|
39
57
|
const Icon = layer.icon;
|
|
40
58
|
const fromTop = i;
|
|
@@ -42,12 +60,12 @@ const IsoStack = ({ layers = DEFAULT_LAYERS, size = 13, gap = 1.6, theme = "dark
|
|
|
42
60
|
return (
|
|
43
61
|
<motion.div
|
|
44
62
|
key={layer.label}
|
|
45
|
-
className="absolute inset-0 rounded-2xl border"
|
|
63
|
+
className="absolute inset-0 rounded-2xl border will-change-transform"
|
|
46
64
|
style={{
|
|
47
65
|
zIndex: n - fromTop,
|
|
48
66
|
background: dark ? `linear-gradient(135deg, color-mix(in srgb, ${layer.color} 22%, #0a0a0a), #0a0a0a)` : `linear-gradient(135deg, color-mix(in srgb, ${layer.color} 30%, #fff), #fff)`,
|
|
49
67
|
borderColor: `color-mix(in srgb, ${layer.color} 60%, transparent)`,
|
|
50
|
-
boxShadow: `0 0 0 1px ${layer.color}22, -
|
|
68
|
+
boxShadow: `0 0 0 1px ${layer.color}22, -10px 10px 16px -8px rgba(0,0,0,${dark ? 0.55 : 0.2})`,
|
|
51
69
|
}}
|
|
52
70
|
animate={{ z: `${lift}rem` }}
|
|
53
71
|
transition={{ type: "spring", stiffness: 200, damping: 22, delay: open ? (n - fromTop) * 0.03 : 0 }}
|
|
@@ -64,12 +82,12 @@ const IsoStack = ({ layers = DEFAULT_LAYERS, size = 13, gap = 1.6, theme = "dark
|
|
|
64
82
|
</div>
|
|
65
83
|
|
|
66
84
|
{/* Labels sit outside the 3D plane so they stay readable. */}
|
|
67
|
-
<div className=
|
|
85
|
+
<div className={`pointer-events-none flex gap-2 ${narrow ? "mt-4 flex-wrap justify-center px-2" : "absolute right-6 top-1/2 -translate-y-1/2 flex-col"}`}>
|
|
68
86
|
{layers.map((layer, i) => (
|
|
69
87
|
<motion.div
|
|
70
88
|
key={layer.label}
|
|
71
|
-
className={`rounded-lg border px-2.5 py-1.5 text-
|
|
72
|
-
animate={{ opacity: open ? 1 : 0, x: open ? 0 : 12, scale: hover === i ? 1.05 : 1 }}
|
|
89
|
+
className={`rounded-lg border px-2.5 py-1.5 ${narrow ? "text-center" : "text-right"} ${dark ? "border-white/10 bg-neutral-950/85 text-white" : "border-black/10 bg-white/90 text-neutral-900"}`}
|
|
90
|
+
animate={{ opacity: open || narrow ? 1 : 0, x: open || narrow ? 0 : 12, scale: hover === i ? 1.05 : 1 }}
|
|
73
91
|
transition={{ delay: open ? i * 0.05 : 0 }}
|
|
74
92
|
>
|
|
75
93
|
<p className="text-[0.78rem] font-semibold" style={{ color: hover === i ? layer.color : undefined }}>
|
|
@@ -39,11 +39,12 @@ function FlowingSpheres({ count, sep, color, pushStrength }) {
|
|
|
39
39
|
const mouseZWorld = -mouse.y * (count * sep / 4);
|
|
40
40
|
|
|
41
41
|
// Animate each sphere's y-position and update its matrix
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
const timeFactor = Math.sin(elapsedTime * 0.5);
|
|
43
|
+
const mesh = instancedMeshRef.current;
|
|
44
|
+
for (let i = 0; i < particles.length; i++) {
|
|
45
|
+
const { x, z } = particles[i];
|
|
44
46
|
|
|
45
47
|
// --- 1. Wave Animation ---
|
|
46
|
-
const timeFactor = Math.sin(elapsedTime * 0.5);
|
|
47
48
|
const wave1 = Math.sin(x * 0.15 + timeFactor * Math.PI);
|
|
48
49
|
const wave2 = Math.sin(z * 1 + timeFactor * Math.PI);
|
|
49
50
|
let y = (wave1 + wave2) * 7;
|
|
@@ -63,8 +64,8 @@ function FlowingSpheres({ count, sep, color, pushStrength }) {
|
|
|
63
64
|
dummy.position.set(x, y, z);
|
|
64
65
|
dummy.updateMatrix();
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
}
|
|
67
|
+
mesh.setMatrixAt(i, dummy.matrix);
|
|
68
|
+
}
|
|
68
69
|
|
|
69
70
|
instancedMeshRef.current.instanceMatrix.needsUpdate = true;
|
|
70
71
|
});
|
|
@@ -75,7 +76,8 @@ function FlowingSpheres({ count, sep, color, pushStrength }) {
|
|
|
75
76
|
ref={instancedMeshRef}
|
|
76
77
|
args={[null, null, count * count]}
|
|
77
78
|
>
|
|
78
|
-
|
|
79
|
+
{/* 8x6 segments look the same as 19x19 at this size, with ~7x fewer triangles. */}
|
|
80
|
+
<sphereGeometry args={[0.2, 8, 6]} />
|
|
79
81
|
<meshStandardMaterial
|
|
80
82
|
color={color}
|
|
81
83
|
emissive={color}
|
|
@@ -37,12 +37,12 @@ const Particles = ({ number = 50, color = '#ffffff', speed = 1 }) => {
|
|
|
37
37
|
};
|
|
38
38
|
|
|
39
39
|
// Main MeteorCard Component
|
|
40
|
-
//
|
|
40
|
+
// Cards share a responsive grid: up to 20rem wide, shrinking to fit narrow screens.
|
|
41
41
|
const MeteorCard = ({ data, particleNumber, particleColor, particleSpeed }) => {
|
|
42
42
|
return (
|
|
43
|
-
<div className="
|
|
43
|
+
<div className="grid w-full justify-center gap-6" style={{ gridTemplateColumns: "repeat(auto-fit, minmax(min(100%, 16rem), 20rem))" }}>
|
|
44
44
|
{data.map((item, index) => (
|
|
45
|
-
<div key={index} className="relative w-
|
|
45
|
+
<div key={index} className="relative w-full min-w-0 bg-slate-800/80 p-0.5 rounded-2xl moving-border-card">
|
|
46
46
|
{/* Card ka content container */}
|
|
47
47
|
<div className="relative bg-slate-900 rounded-[calc(1rem-2px)] p-3 flex flex-col overflow-hidden">
|
|
48
48
|
{/* Background Effects */}
|
|
@@ -59,13 +59,13 @@ const MeteorCard = ({ data, particleNumber, particleColor, particleSpeed }) => {
|
|
|
59
59
|
<div className="absolute -top-1/4 -left-1/4 w-3/4 h-1/2 bg-blue-400/60 rounded-full blur-[90px] pointer-events-none z-0"></div>
|
|
60
60
|
|
|
61
61
|
{/* Image */}
|
|
62
|
-
<div className="relative
|
|
62
|
+
<div className="relative aspect-[4/3] w-full rounded-xl overflow-hidden z-10 ring-1 ring-white/10">
|
|
63
63
|
<img src={item.imageUrl} alt={item.title} className="w-full h-full object-cover object-[center_25%]" />
|
|
64
64
|
</div>
|
|
65
65
|
|
|
66
66
|
{/* Content */}
|
|
67
67
|
<div className="relative z-10 px-2 pt-4 pb-2 text-center">
|
|
68
|
-
<h3 className="text-
|
|
68
|
+
<h3 className="text-xl font-bold sm:text-2xl mb-2 text-white tracking-tight">{item.title}</h3>
|
|
69
69
|
<p className="text-sm leading-relaxed text-gray-400 mb-5">{item.description}</p>
|
|
70
70
|
<button className="group relative inline-flex w-full items-center justify-center whitespace-nowrap px-6 py-2.5 bg-lime-600 text-white font-semibold rounded-lg overflow-hidden transition-all duration-300 hover:bg-lime-700">
|
|
71
71
|
<span>{item.buttonText}</span>
|
|
@@ -148,7 +148,7 @@ const OrbitIcons = ({ rings = DEFAULT_RINGS, size = 28, tilt = 0.55, speed = 1,
|
|
|
148
148
|
return (
|
|
149
149
|
<div key={idx} ref={(e) => (nodes.current[idx] = e)} className="group absolute left-1/2 top-1/2 will-change-transform">
|
|
150
150
|
<div
|
|
151
|
-
className="grid place-items-center rounded-2xl border border-white/10 bg-neutral-900/90 text-white
|
|
151
|
+
className="grid place-items-center rounded-2xl border border-white/10 bg-neutral-900/90 text-white transition-colors duration-300 group-hover:border-[var(--orbit-accent)]"
|
|
152
152
|
style={{ width: "var(--orbit-item, 2.75rem)", aspectRatio: "1" }}
|
|
153
153
|
title={it.label}
|
|
154
154
|
>
|
|
@@ -25,53 +25,54 @@ const ParallaxCard = ({ cardData = [], tiltEnable, glareEnable, scale, perspecti
|
|
|
25
25
|
|
|
26
26
|
if (!cardElement) return;
|
|
27
27
|
|
|
28
|
+
// quickTo builds each tween once and just retargets it, instead of
|
|
29
|
+
// creating six new tweens on every mouse move.
|
|
30
|
+
const to = (el, prop, duration) => (el ? gsap.quickTo(el, prop, { duration, ease: "power3.out" }) : () => {});
|
|
31
|
+
const card = { rx: to(cardElement, "rotationX", 0.8), ry: to(cardElement, "rotationY", 0.8), s: to(cardElement, "scale", 0.8) };
|
|
32
|
+
const layers = [
|
|
33
|
+
[titleElement, 0.8, 0.6],
|
|
34
|
+
[imageElement, 1.5, 0.7],
|
|
35
|
+
[descElement, 1.2, 0.8],
|
|
36
|
+
[buttonElement, 1.1, 0.9],
|
|
37
|
+
[linkElement, 1.1, 0.9],
|
|
38
|
+
]
|
|
39
|
+
.filter(([el]) => el)
|
|
40
|
+
.map(([el, k, d]) => ({ k, x: to(el, "x", d), y: to(el, "y", d) }));
|
|
41
|
+
let rect = null;
|
|
42
|
+
|
|
43
|
+
const handleMouseEnter = () => {
|
|
44
|
+
rect = cardElement.getBoundingClientRect();
|
|
45
|
+
};
|
|
28
46
|
const handleMouseMove = (e) => {
|
|
29
47
|
if (!tiltEnable) return;
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
rotationY: rotateY,
|
|
40
|
-
scale: scale,
|
|
41
|
-
ease: "power3.out",
|
|
48
|
+
const r = rect ?? (rect = cardElement.getBoundingClientRect());
|
|
49
|
+
const rotateX = ((e.clientY - r.top) / r.height - 0.5) * -30;
|
|
50
|
+
const rotateY = ((e.clientX - r.left) / r.width - 0.5) * 30;
|
|
51
|
+
card.rx(rotateX);
|
|
52
|
+
card.ry(rotateY);
|
|
53
|
+
card.s(scale);
|
|
54
|
+
layers.forEach((l) => {
|
|
55
|
+
l.x(rotateY * l.k);
|
|
56
|
+
l.y(-rotateX * l.k);
|
|
42
57
|
});
|
|
43
|
-
|
|
44
|
-
if (titleElement) gsap.to(titleElement, { duration: 0.7, x: rotateY * 0.8, y: -rotateX * 0.8 });
|
|
45
|
-
if (imageElement) gsap.to(imageElement, { duration: 0.8, x: rotateY * 1.5, y: -rotateX * 1.5 });
|
|
46
|
-
if (descElement) gsap.to(descElement, { duration: 0.9, x: rotateY * 1.2, y: -rotateX * 1.2 });
|
|
47
|
-
if (buttonElement) gsap.to(buttonElement, { duration: 1, x: rotateY * 1.1, y: -rotateX * 1.1 });
|
|
48
|
-
if (linkElement) gsap.to(linkElement, { duration: 1, x: rotateY * 1.1, y: -rotateX * 1.1 });
|
|
49
58
|
};
|
|
50
59
|
|
|
51
60
|
const handleMouseLeave = () => {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
rotationY: 0,
|
|
57
|
-
scale: 1,
|
|
58
|
-
ease: "elastic.out(1, 0.5)",
|
|
59
|
-
});
|
|
60
|
-
gsap.to([titleElement, imageElement, descElement, buttonElement, linkElement].filter(Boolean), {
|
|
61
|
-
duration: 1.2,
|
|
62
|
-
x: 0,
|
|
63
|
-
y: 0,
|
|
64
|
-
ease: "elastic.out(1, 0.5)",
|
|
65
|
-
});
|
|
66
|
-
}
|
|
61
|
+
rect = null;
|
|
62
|
+
if (!tiltEnable) return;
|
|
63
|
+
gsap.to(cardElement, { duration: 1.2, rotationX: 0, rotationY: 0, scale: 1, ease: "elastic.out(1, 0.5)", overwrite: "auto" });
|
|
64
|
+
gsap.to([titleElement, imageElement, descElement, buttonElement, linkElement].filter(Boolean), { duration: 1.2, x: 0, y: 0, ease: "elastic.out(1, 0.5)", overwrite: "auto" });
|
|
67
65
|
};
|
|
68
66
|
|
|
67
|
+
cardElement.addEventListener("mouseenter", handleMouseEnter);
|
|
69
68
|
cardElement.addEventListener("mousemove", handleMouseMove, { passive: true });
|
|
70
69
|
cardElement.addEventListener("mouseleave", handleMouseLeave);
|
|
71
70
|
|
|
72
71
|
cleanups.push(() => {
|
|
72
|
+
cardElement.removeEventListener("mouseenter", handleMouseEnter);
|
|
73
73
|
cardElement.removeEventListener("mousemove", handleMouseMove);
|
|
74
74
|
cardElement.removeEventListener("mouseleave", handleMouseLeave);
|
|
75
|
+
gsap.killTweensOf([cardElement, titleElement, imageElement, descElement, buttonElement, linkElement].filter(Boolean));
|
|
75
76
|
});
|
|
76
77
|
});
|
|
77
78
|
return () => cleanups.forEach((fn) => fn());
|