apex-ui-kit 2.0.7 → 2.1.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/AccordionMarquee/AccordionMarquee.jsx +74 -53
- package/dist/ApexUI-Kit/templates/BeamBar/BeamBar.jsx +5 -3
- package/dist/ApexUI-Kit/templates/BluePrintBackground/BluePrintBackground.jsx +14 -2
- package/dist/ApexUI-Kit/templates/CardStack/CardStack.jsx +56 -28
- package/dist/ApexUI-Kit/templates/Cards/BasicCard.jsx +3 -3
- package/dist/ApexUI-Kit/templates/DragCarousel/DragCarousel.jsx +1 -1
- package/dist/ApexUI-Kit/templates/EdgeGlowForm/EdgeGlowFrom.jsx +116 -98
- package/dist/ApexUI-Kit/templates/FlipProfile/FlipProfile.jsx +70 -37
- package/dist/ApexUI-Kit/templates/GlareCard/GlareCard.jsx +2 -2
- package/dist/ApexUI-Kit/templates/HoverText/HoverText.jsx +14 -13
- package/dist/ApexUI-Kit/templates/HyperCard/HyperCard.jsx +59 -42
- package/dist/ApexUI-Kit/templates/KineticThreadsBackground/KineticThreadBackground.jsx +17 -2
- package/dist/ApexUI-Kit/templates/LensFlareBackground/LensFlareBackground.jsx +18 -3
- package/dist/ApexUI-Kit/templates/LuminousParticleOcean/LuminousParticleOcean.jsx +15 -1
- package/dist/ApexUI-Kit/templates/MaskView/MaskView.jsx +345 -0
- package/dist/ApexUI-Kit/templates/MeteorCard/MeteorCard.jsx +34 -45
- package/dist/ApexUI-Kit/templates/NavDropDown/NavDropDown.jsx +272 -101
- package/dist/ApexUI-Kit/templates/NavMenu/NavMenu.jsx +8 -2
- package/dist/ApexUI-Kit/templates/OrbsBackground/FloatingOrbs.jsx +59 -59
- package/dist/ApexUI-Kit/templates/ParallaxCard/ParallaxCard.jsx +13 -11
- package/dist/ApexUI-Kit/templates/ParallaxCarousel/ParallaxCarousel.jsx +9 -9
- package/dist/ApexUI-Kit/templates/ParallaxProfile/ParallaxProfile.jsx +41 -38
- package/dist/ApexUI-Kit/templates/PointerFollower/PointerFollower.jsx +7 -7
- package/dist/ApexUI-Kit/templates/SmartGridCard/SmartGridCard.jsx +13 -11
- package/dist/ApexUI-Kit/templates/SmokeBackground/SmokeBackground.jsx +13 -2
- package/dist/ApexUI-Kit/templates/Testimonials/Testimonials.jsx +11 -8
- package/dist/ApexUI-Kit/templates/ThemeToggle/ThemeAnimation.js +40 -69
- package/dist/ApexUI-Kit/templates/ThemeToggle/ThemeToggle.css +1 -1
- package/dist/ApexUI-Kit/templates/ThemeToggle/ThemeToggle.jsx +25 -7
- package/dist/ApexUI-Kit/templates/ToolTip/ToolTip.jsx +71 -52
- package/dist/cli/index.js +24 -13
- package/package.json +2 -2
- package/dist/ApexUI-Kit/templates/WaterDropReveal/WaterDropReveal.jsx +0 -151
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect } from "react";
|
|
2
|
+
import gsap from "gsap";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Mask View: three ways to reveal content through a mask.
|
|
6
|
+
* variant="window" – big letters are a window onto an image; it pans with the pointer, tap opens it fully.
|
|
7
|
+
* variant="spotlight" – near-hidden text lit by a soft spotlight that follows the pointer (wanders on its own when idle).
|
|
8
|
+
* variant="wipe" – text under steamed-up glass; wipe it clear with the pointer or a finger, it slowly fogs back.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const DEFAULT_IMAGE = "https://ik.imagekit.io/ny6yinyut/apexui/assets/ironman.png?updatedAt=1792562203671";
|
|
12
|
+
const DEFAULT_TEXT = {
|
|
13
|
+
window: "MASK\nVIEW",
|
|
14
|
+
spotlight: "Look through the mask.\nOnly what sits under the light\ncomes into focus.",
|
|
15
|
+
wipe: "Wipe the glass.\nThe words underneath\nare waiting for you.",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const textClass = "text-center text-2xl sm:text-4xl font-semibold leading-tight tracking-tight";
|
|
19
|
+
|
|
20
|
+
const Lines = ({ text }) => text.split("\n").map((line, i) => <span key={i} className="block">{line}</span>);
|
|
21
|
+
|
|
22
|
+
// Shared card: dark panel, corner label and a hint that hides while the effect is in use.
|
|
23
|
+
const Frame = ({ boxRef, accent, label, hint, active, className = "", children, ...handlers }) => (
|
|
24
|
+
<div
|
|
25
|
+
ref={boxRef}
|
|
26
|
+
className={`relative isolate w-full max-w-2xl min-h-[300px] overflow-hidden rounded-3xl border border-white/10 bg-[#07070c] select-none ${className}`}
|
|
27
|
+
{...handlers}
|
|
28
|
+
>
|
|
29
|
+
<div
|
|
30
|
+
className="pointer-events-none absolute inset-0 opacity-[0.06]"
|
|
31
|
+
style={{ backgroundImage: "radial-gradient(rgba(255,255,255,0.9) 1px, transparent 1px)", backgroundSize: "18px 18px" }}
|
|
32
|
+
/>
|
|
33
|
+
{children}
|
|
34
|
+
<span className="pointer-events-none absolute left-5 top-4 z-30 flex items-center gap-2 font-mono text-[10px] uppercase tracking-[0.2em] text-white/45">
|
|
35
|
+
<span className="h-1.5 w-1.5 rounded-full" style={{ backgroundColor: accent }} />
|
|
36
|
+
{label}
|
|
37
|
+
</span>
|
|
38
|
+
<span
|
|
39
|
+
className={`pointer-events-none absolute bottom-4 left-1/2 z-30 -translate-x-1/2 whitespace-nowrap rounded-full border border-white/10 bg-black/40 px-3.5 py-1.5 text-xs text-white/65 backdrop-blur-md transition-all duration-500 ${active ? "translate-y-3 opacity-0" : "opacity-100"}`}
|
|
40
|
+
>
|
|
41
|
+
{hint}
|
|
42
|
+
</span>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
/* ---------------------------------------------------------------- window */
|
|
47
|
+
const TextWindow = ({ text, image, accent, speed }) => {
|
|
48
|
+
const boxRef = useRef(null);
|
|
49
|
+
const pan = useRef({ x: 50, y: 50 });
|
|
50
|
+
const [hovered, setHovered] = useState(false);
|
|
51
|
+
const [open, setOpen] = useState(false);
|
|
52
|
+
const [origin, setOrigin] = useState({ x: 50, y: 50 });
|
|
53
|
+
|
|
54
|
+
const paint = () => {
|
|
55
|
+
boxRef.current?.style.setProperty("--px", `${pan.current.x}%`);
|
|
56
|
+
boxRef.current?.style.setProperty("--py", `${pan.current.y}%`);
|
|
57
|
+
};
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
const state = pan.current;
|
|
60
|
+
paint();
|
|
61
|
+
return () => gsap.killTweensOf(state);
|
|
62
|
+
}, []);
|
|
63
|
+
|
|
64
|
+
const handleMove = (e) => {
|
|
65
|
+
const rect = boxRef.current.getBoundingClientRect();
|
|
66
|
+
// Pan the image the opposite way to the pointer, for a little depth.
|
|
67
|
+
gsap.to(pan.current, {
|
|
68
|
+
x: 100 - ((e.clientX - rect.left) / rect.width) * 100,
|
|
69
|
+
y: 100 - ((e.clientY - rect.top) / rect.height) * 100,
|
|
70
|
+
duration: speed * 2,
|
|
71
|
+
ease: "power3.out",
|
|
72
|
+
overwrite: "auto",
|
|
73
|
+
onUpdate: paint,
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
const handleLeave = () => {
|
|
77
|
+
setHovered(false);
|
|
78
|
+
gsap.to(pan.current, { x: 50, y: 50, duration: 0.9, ease: "power3.out", onUpdate: paint });
|
|
79
|
+
};
|
|
80
|
+
const handleClick = (e) => {
|
|
81
|
+
const rect = boxRef.current.getBoundingClientRect();
|
|
82
|
+
setOrigin({ x: ((e.clientX - rect.left) / rect.width) * 100, y: ((e.clientY - rect.top) / rect.height) * 100 });
|
|
83
|
+
setOpen((o) => !o);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<Frame
|
|
88
|
+
boxRef={boxRef}
|
|
89
|
+
accent={accent}
|
|
90
|
+
label="Mask View"
|
|
91
|
+
hint={open ? "Tap to close" : "Move to pan · tap to open"}
|
|
92
|
+
active={hovered && !open}
|
|
93
|
+
className="cursor-pointer touch-pan-y"
|
|
94
|
+
onPointerEnter={() => setHovered(true)}
|
|
95
|
+
onPointerMove={handleMove}
|
|
96
|
+
onPointerLeave={handleLeave}
|
|
97
|
+
onClick={handleClick}
|
|
98
|
+
>
|
|
99
|
+
{/* Full image, opened with a circle from the tap point */}
|
|
100
|
+
<div
|
|
101
|
+
className="absolute inset-0 z-10 transition-[clip-path] duration-700 ease-[cubic-bezier(0.65,0,0.35,1)]"
|
|
102
|
+
style={{
|
|
103
|
+
backgroundImage: `url("${image}")`,
|
|
104
|
+
backgroundSize: "cover",
|
|
105
|
+
backgroundPosition: "var(--px) var(--py)",
|
|
106
|
+
clipPath: `circle(${open ? "150%" : "0%"} at ${origin.x}% ${origin.y}%)`,
|
|
107
|
+
}}
|
|
108
|
+
/>
|
|
109
|
+
{/* Letters as the window */}
|
|
110
|
+
<div className="absolute inset-0 z-20 flex items-center justify-center px-4">
|
|
111
|
+
<p
|
|
112
|
+
className={`text-center text-[clamp(4rem,19vw,9rem)] font-black uppercase leading-[0.85] tracking-[-0.04em] transition-[color,text-shadow] duration-500 ${open ? "text-white [text-shadow:0_8px_40px_rgba(0,0,0,0.6)]" : "text-transparent"}`}
|
|
113
|
+
style={open ? undefined : {
|
|
114
|
+
backgroundImage: `url("${image}")`,
|
|
115
|
+
backgroundSize: "180%",
|
|
116
|
+
backgroundPosition: "var(--px) var(--py)",
|
|
117
|
+
WebkitBackgroundClip: "text",
|
|
118
|
+
backgroundClip: "text",
|
|
119
|
+
WebkitTextStroke: "1px rgba(255,255,255,0.12)",
|
|
120
|
+
}}
|
|
121
|
+
>
|
|
122
|
+
<Lines text={text} />
|
|
123
|
+
</p>
|
|
124
|
+
</div>
|
|
125
|
+
</Frame>
|
|
126
|
+
);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/* ------------------------------------------------------------- spotlight */
|
|
130
|
+
const Spotlight = ({ text, accent, speed }) => {
|
|
131
|
+
const boxRef = useRef(null);
|
|
132
|
+
const light = useRef({ x: 0, y: 0, r: 0 });
|
|
133
|
+
const idle = useRef(null);
|
|
134
|
+
const [active, setActive] = useState(false);
|
|
135
|
+
|
|
136
|
+
const paint = () => {
|
|
137
|
+
const el = boxRef.current;
|
|
138
|
+
if (!el) return;
|
|
139
|
+
el.style.setProperty("--x", `${light.current.x}px`);
|
|
140
|
+
el.style.setProperty("--y", `${light.current.y}px`);
|
|
141
|
+
el.style.setProperty("--r", `${light.current.r}px`);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// When nobody is pointing, the light drifts slowly across the text.
|
|
145
|
+
const startIdle = () => {
|
|
146
|
+
const el = boxRef.current;
|
|
147
|
+
if (!el) return;
|
|
148
|
+
const w = el.offsetWidth, h = el.offsetHeight;
|
|
149
|
+
idle.current?.kill();
|
|
150
|
+
idle.current = gsap.timeline({ repeat: -1, yoyo: true, defaults: { duration: 2.6, ease: "sine.inOut", onUpdate: paint } })
|
|
151
|
+
.to(light.current, { x: w * 0.3, y: h * 0.42, r: Math.min(120, w * 0.22) })
|
|
152
|
+
.to(light.current, { x: w * 0.7, y: h * 0.58 });
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
const el = boxRef.current;
|
|
157
|
+
const state = light.current;
|
|
158
|
+
if (!el) return;
|
|
159
|
+
state.x = el.offsetWidth * 0.3;
|
|
160
|
+
state.y = el.offsetHeight * 0.42;
|
|
161
|
+
paint();
|
|
162
|
+
startIdle();
|
|
163
|
+
return () => {
|
|
164
|
+
idle.current?.kill();
|
|
165
|
+
gsap.killTweensOf(state);
|
|
166
|
+
};
|
|
167
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
168
|
+
}, []);
|
|
169
|
+
|
|
170
|
+
const handleEnter = () => {
|
|
171
|
+
setActive(true);
|
|
172
|
+
idle.current?.kill();
|
|
173
|
+
const w = boxRef.current.offsetWidth;
|
|
174
|
+
gsap.to(light.current, { r: Math.min(170, w * 0.3), duration: 0.6, ease: "power3.out", onUpdate: paint });
|
|
175
|
+
};
|
|
176
|
+
const handleMove = (e) => {
|
|
177
|
+
if (!active) handleEnter();
|
|
178
|
+
const rect = boxRef.current.getBoundingClientRect();
|
|
179
|
+
gsap.to(light.current, { x: e.clientX - rect.left, y: e.clientY - rect.top, duration: speed, ease: "power3.out", overwrite: "auto", onUpdate: paint });
|
|
180
|
+
};
|
|
181
|
+
const handleLeave = () => {
|
|
182
|
+
setActive(false);
|
|
183
|
+
startIdle();
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const mask = "radial-gradient(circle var(--r) at var(--x) var(--y), #000 0%, rgba(0,0,0,0.85) 35%, transparent 100%)";
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<Frame
|
|
190
|
+
boxRef={boxRef}
|
|
191
|
+
accent={accent}
|
|
192
|
+
label="Spotlight"
|
|
193
|
+
hint="Hover or touch to shine the light"
|
|
194
|
+
active={active}
|
|
195
|
+
className="cursor-none touch-pan-y"
|
|
196
|
+
onPointerEnter={handleEnter}
|
|
197
|
+
onPointerMove={handleMove}
|
|
198
|
+
onPointerLeave={handleLeave}
|
|
199
|
+
onPointerCancel={handleLeave}
|
|
200
|
+
>
|
|
201
|
+
{/* Text in the dark */}
|
|
202
|
+
<div className="absolute inset-0 flex items-center justify-center px-6 sm:px-10">
|
|
203
|
+
<p className={`${textClass} text-white/[0.07]`}><Lines text={text} /></p>
|
|
204
|
+
</div>
|
|
205
|
+
{/* Lit text */}
|
|
206
|
+
<div className="absolute inset-0 flex items-center justify-center px-6 sm:px-10" style={{ WebkitMaskImage: mask, maskImage: mask }} aria-hidden="true">
|
|
207
|
+
<p className={`${textClass} text-white`}><Lines text={text} /></p>
|
|
208
|
+
</div>
|
|
209
|
+
{/* Faint pool of light on the surface */}
|
|
210
|
+
<div
|
|
211
|
+
className="pointer-events-none absolute inset-0"
|
|
212
|
+
style={{ background: "radial-gradient(circle calc(var(--r) * 1.4) at var(--x) var(--y), rgba(255,255,255,0.05), transparent 70%)" }}
|
|
213
|
+
/>
|
|
214
|
+
</Frame>
|
|
215
|
+
);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/* ------------------------------------------------------------------ wipe */
|
|
219
|
+
const FOG = "#10101a";
|
|
220
|
+
|
|
221
|
+
const WipeReveal = ({ text, accent }) => {
|
|
222
|
+
const boxRef = useRef(null);
|
|
223
|
+
const canvasRef = useRef(null);
|
|
224
|
+
const last = useRef(null);
|
|
225
|
+
const regrowUntil = useRef(0);
|
|
226
|
+
const frame = useRef(0);
|
|
227
|
+
const [active, setActive] = useState(false);
|
|
228
|
+
|
|
229
|
+
useEffect(() => {
|
|
230
|
+
const canvas = canvasRef.current;
|
|
231
|
+
const box = boxRef.current;
|
|
232
|
+
if (!canvas || !box) return;
|
|
233
|
+
const ctx = canvas.getContext("2d");
|
|
234
|
+
|
|
235
|
+
const fill = () => {
|
|
236
|
+
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
237
|
+
canvas.width = box.offsetWidth * dpr;
|
|
238
|
+
canvas.height = box.offsetHeight * dpr;
|
|
239
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
240
|
+
ctx.globalCompositeOperation = "source-over";
|
|
241
|
+
ctx.fillStyle = FOG;
|
|
242
|
+
ctx.fillRect(0, 0, box.offsetWidth, box.offsetHeight);
|
|
243
|
+
};
|
|
244
|
+
fill();
|
|
245
|
+
const ro = new ResizeObserver(fill);
|
|
246
|
+
ro.observe(box);
|
|
247
|
+
return () => {
|
|
248
|
+
ro.disconnect();
|
|
249
|
+
cancelAnimationFrame(frame.current);
|
|
250
|
+
};
|
|
251
|
+
}, []);
|
|
252
|
+
|
|
253
|
+
// Fog creeps back over cleared glass for a few seconds after the last wipe.
|
|
254
|
+
const regrow = () => {
|
|
255
|
+
const canvas = canvasRef.current;
|
|
256
|
+
const box = boxRef.current;
|
|
257
|
+
if (!canvas || !box) return;
|
|
258
|
+
const ctx = canvas.getContext("2d");
|
|
259
|
+
ctx.globalCompositeOperation = "source-over";
|
|
260
|
+
ctx.globalAlpha = 0.018;
|
|
261
|
+
ctx.fillStyle = FOG;
|
|
262
|
+
ctx.fillRect(0, 0, box.offsetWidth, box.offsetHeight);
|
|
263
|
+
ctx.globalAlpha = 1;
|
|
264
|
+
frame.current = performance.now() < regrowUntil.current ? requestAnimationFrame(regrow) : 0;
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const wipe = (x, y) => {
|
|
268
|
+
const ctx = canvasRef.current.getContext("2d");
|
|
269
|
+
const radius = Math.max(36, Math.min(60, boxRef.current.offsetWidth * 0.09));
|
|
270
|
+
const from = last.current ?? { x, y };
|
|
271
|
+
const steps = Math.max(1, Math.ceil(Math.hypot(x - from.x, y - from.y) / (radius / 3)));
|
|
272
|
+
ctx.globalCompositeOperation = "destination-out";
|
|
273
|
+
// Stamp soft brush dots along the path so fast moves leave no gaps.
|
|
274
|
+
for (let i = 1; i <= steps; i++) {
|
|
275
|
+
const px = from.x + ((x - from.x) * i) / steps;
|
|
276
|
+
const py = from.y + ((y - from.y) * i) / steps;
|
|
277
|
+
const brush = ctx.createRadialGradient(px, py, 0, px, py, radius);
|
|
278
|
+
brush.addColorStop(0, "rgba(0,0,0,0.9)");
|
|
279
|
+
brush.addColorStop(1, "rgba(0,0,0,0)");
|
|
280
|
+
ctx.fillStyle = brush;
|
|
281
|
+
ctx.fillRect(px - radius, py - radius, radius * 2, radius * 2);
|
|
282
|
+
}
|
|
283
|
+
last.current = { x, y };
|
|
284
|
+
regrowUntil.current = performance.now() + 4000;
|
|
285
|
+
if (!frame.current) frame.current = requestAnimationFrame(regrow);
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const handleMove = (e) => {
|
|
289
|
+
const rect = boxRef.current.getBoundingClientRect();
|
|
290
|
+
setActive(true);
|
|
291
|
+
wipe(e.clientX - rect.left, e.clientY - rect.top);
|
|
292
|
+
};
|
|
293
|
+
const handleLeave = () => {
|
|
294
|
+
last.current = null;
|
|
295
|
+
setActive(false);
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
return (
|
|
299
|
+
<Frame
|
|
300
|
+
boxRef={boxRef}
|
|
301
|
+
accent={accent}
|
|
302
|
+
label="Wipe Reveal"
|
|
303
|
+
hint="Wipe the glass with your pointer or finger"
|
|
304
|
+
active={active}
|
|
305
|
+
className="cursor-crosshair touch-pan-y"
|
|
306
|
+
onPointerMove={handleMove}
|
|
307
|
+
onPointerLeave={handleLeave}
|
|
308
|
+
onPointerCancel={handleLeave}
|
|
309
|
+
>
|
|
310
|
+
{/* Clear text under the glass */}
|
|
311
|
+
<div className="absolute inset-0 flex items-center justify-center px-6 sm:px-10">
|
|
312
|
+
<p className={`${textClass} text-white`}><Lines text={text} /></p>
|
|
313
|
+
</div>
|
|
314
|
+
{/* Steam on the glass */}
|
|
315
|
+
<canvas ref={canvasRef} className="pointer-events-none absolute inset-0 h-full w-full opacity-[0.93]" />
|
|
316
|
+
{/* Condensation droplets */}
|
|
317
|
+
<div
|
|
318
|
+
className="pointer-events-none absolute inset-0 opacity-30"
|
|
319
|
+
style={{
|
|
320
|
+
backgroundImage: "radial-gradient(rgba(255,255,255,0.35) 1px, transparent 1.5px), radial-gradient(rgba(255,255,255,0.2) 1px, transparent 1.5px)",
|
|
321
|
+
backgroundSize: "23px 29px, 37px 31px",
|
|
322
|
+
backgroundPosition: "0 0, 11px 7px",
|
|
323
|
+
}}
|
|
324
|
+
/>
|
|
325
|
+
</Frame>
|
|
326
|
+
);
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const MaskView = ({
|
|
330
|
+
variant = "spotlight",
|
|
331
|
+
text,
|
|
332
|
+
image = DEFAULT_IMAGE,
|
|
333
|
+
accentColor,
|
|
334
|
+
dropColor, // older name for accentColor, still accepted
|
|
335
|
+
animationSpeed = 0.4,
|
|
336
|
+
}) => {
|
|
337
|
+
const kind = DEFAULT_TEXT[variant] ? variant : "spotlight";
|
|
338
|
+
const content = text ?? DEFAULT_TEXT[kind];
|
|
339
|
+
const accent = accentColor ?? dropColor ?? "#a855f7";
|
|
340
|
+
if (kind === "window") return <TextWindow text={content} image={image} accent={accent} speed={animationSpeed} />;
|
|
341
|
+
if (kind === "wipe") return <WipeReveal text={content} accent={accent} />;
|
|
342
|
+
return <Spotlight text={content} accent={accent} speed={animationSpeed} />;
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
export default MaskView;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
2
|
import './MeteorCard.css';
|
|
3
3
|
const ArrowRightIcon = () => (
|
|
4
4
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"
|
|
@@ -11,51 +11,40 @@ const ArrowRightIcon = () => (
|
|
|
11
11
|
);
|
|
12
12
|
|
|
13
13
|
// Meteor Effect Component
|
|
14
|
+
// Positions are picked once (useMemo), so re-renders don't make the meteors jump.
|
|
14
15
|
const Meteors = ({ number = 20 }) => {
|
|
15
|
-
const meteors = [...Array(number)].map((
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
animationDelay: `${Math.random() * 8}s`,
|
|
23
|
-
animationDuration: `${Math.floor(Math.random() * 6) + 4}s`,
|
|
24
|
-
}}
|
|
25
|
-
></span>
|
|
26
|
-
));
|
|
27
|
-
return <>{meteors}</>;
|
|
16
|
+
const meteors = useMemo(() => [...Array(number)].map(() => ({
|
|
17
|
+
top: `${Math.random() * 100}%`,
|
|
18
|
+
left: `${Math.random() * 100}%`,
|
|
19
|
+
animationDelay: `${Math.random() * 8}s`,
|
|
20
|
+
animationDuration: `${Math.floor(Math.random() * 6) + 4}s`,
|
|
21
|
+
})), [number]);
|
|
22
|
+
return <>{meteors.map((style, i) => <span key={i} className="meteor-bg" style={style}></span>)}</>;
|
|
28
23
|
};
|
|
29
24
|
|
|
30
25
|
// Particle Effect Component
|
|
31
26
|
const Particles = ({ number = 50, color = '#ffffff', speed = 1 }) => {
|
|
32
|
-
const particles = [...Array(number)].map((
|
|
27
|
+
const particles = useMemo(() => [...Array(number)].map(() => {
|
|
33
28
|
const animationDuration = (Math.random() * 5 + 3) / speed;
|
|
34
|
-
return
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
animationDelay: `${Math.random() * animationDuration}s`,
|
|
43
|
-
animationDuration: `${animationDuration}s`,
|
|
44
|
-
}}
|
|
45
|
-
></span>
|
|
46
|
-
);
|
|
47
|
-
});
|
|
48
|
-
return <>{particles}</>;
|
|
29
|
+
return {
|
|
30
|
+
top: `${Math.random() * 100}%`,
|
|
31
|
+
left: `${Math.random() * 100}%`,
|
|
32
|
+
animationDelay: `${Math.random() * animationDuration}s`,
|
|
33
|
+
animationDuration: `${animationDuration}s`,
|
|
34
|
+
};
|
|
35
|
+
}), [number, speed]);
|
|
36
|
+
return <>{particles.map((style, i) => <span key={i} className="particle-bg" style={{ '--particle-color': color, ...style }}></span>)}</>;
|
|
49
37
|
};
|
|
50
38
|
|
|
51
39
|
// Main MeteorCard Component
|
|
40
|
+
// A fixed-size card (image on top, content below) so it looks the same on every screen.
|
|
52
41
|
const MeteorCard = ({ data, particleNumber, particleColor, particleSpeed }) => {
|
|
53
42
|
return (
|
|
54
|
-
|
|
43
|
+
<div className="flex flex-wrap justify-center gap-6">
|
|
55
44
|
{data.map((item, index) => (
|
|
56
|
-
<div key={index} className="relative w-
|
|
45
|
+
<div key={index} className="relative w-[20rem] max-w-full bg-slate-800/80 p-0.5 rounded-2xl moving-border-card">
|
|
57
46
|
{/* Card ka content container */}
|
|
58
|
-
<div className="relative bg-slate-900 rounded-
|
|
47
|
+
<div className="relative bg-slate-900 rounded-[calc(1rem-2px)] p-3 flex flex-col overflow-hidden">
|
|
59
48
|
{/* Background Effects */}
|
|
60
49
|
<div className="absolute inset-0 w-full h-full z-0">
|
|
61
50
|
<Meteors number={5} />
|
|
@@ -66,27 +55,27 @@ const MeteorCard = ({ data, particleNumber, particleColor, particleSpeed }) => {
|
|
|
66
55
|
/>
|
|
67
56
|
</div>
|
|
68
57
|
|
|
69
|
-
{/* Spotlight Effect
|
|
70
|
-
<div className="
|
|
58
|
+
{/* Spotlight Effect */}
|
|
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>
|
|
71
60
|
|
|
72
|
-
{/*
|
|
73
|
-
<div className="relative
|
|
74
|
-
<img src={item.imageUrl} alt={item.title} className="w-full h-full object-cover
|
|
61
|
+
{/* Image */}
|
|
62
|
+
<div className="relative h-[13.75rem] w-full rounded-xl overflow-hidden z-10 ring-1 ring-white/10">
|
|
63
|
+
<img src={item.imageUrl} alt={item.title} className="w-full h-full object-cover object-[center_25%]" />
|
|
75
64
|
</div>
|
|
76
65
|
|
|
77
|
-
{/*
|
|
78
|
-
<div className="relative
|
|
79
|
-
<h3 className="text-
|
|
80
|
-
<p className="text-gray-400 mb-
|
|
81
|
-
<button className="group relative inline-flex
|
|
82
|
-
<span
|
|
66
|
+
{/* Content */}
|
|
67
|
+
<div className="relative z-10 px-2 pt-4 pb-2 text-center">
|
|
68
|
+
<h3 className="text-2xl font-bold mb-2 text-white tracking-tight">{item.title}</h3>
|
|
69
|
+
<p className="text-sm leading-relaxed text-gray-400 mb-5">{item.description}</p>
|
|
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
|
+
<span>{item.buttonText}</span>
|
|
83
72
|
<ArrowRightIcon />
|
|
84
73
|
</button>
|
|
85
74
|
</div>
|
|
86
75
|
</div>
|
|
87
76
|
</div>
|
|
88
77
|
))}
|
|
89
|
-
|
|
78
|
+
</div>
|
|
90
79
|
);
|
|
91
80
|
};
|
|
92
81
|
|