apex-ui-kit 2.0.7 → 2.1.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.
Files changed (32) hide show
  1. package/dist/ApexUI-Kit/templates/AccordionMarquee/AccordionMarquee.jsx +74 -53
  2. package/dist/ApexUI-Kit/templates/BeamBar/BeamBar.jsx +5 -3
  3. package/dist/ApexUI-Kit/templates/BluePrintBackground/BluePrintBackground.jsx +14 -2
  4. package/dist/ApexUI-Kit/templates/CardStack/CardStack.jsx +56 -28
  5. package/dist/ApexUI-Kit/templates/Cards/BasicCard.jsx +3 -3
  6. package/dist/ApexUI-Kit/templates/DragCarousel/DragCarousel.jsx +1 -1
  7. package/dist/ApexUI-Kit/templates/EdgeGlowForm/EdgeGlowFrom.jsx +116 -98
  8. package/dist/ApexUI-Kit/templates/FlipProfile/FlipProfile.jsx +69 -37
  9. package/dist/ApexUI-Kit/templates/GlareCard/GlareCard.jsx +2 -2
  10. package/dist/ApexUI-Kit/templates/HoverText/HoverText.jsx +14 -13
  11. package/dist/ApexUI-Kit/templates/HyperCard/HyperCard.jsx +59 -42
  12. package/dist/ApexUI-Kit/templates/KineticThreadsBackground/KineticThreadBackground.jsx +17 -2
  13. package/dist/ApexUI-Kit/templates/LensFlareBackground/LensFlareBackground.jsx +18 -3
  14. package/dist/ApexUI-Kit/templates/LuminousParticleOcean/LuminousParticleOcean.jsx +15 -1
  15. package/dist/ApexUI-Kit/templates/MaskView/MaskView.jsx +345 -0
  16. package/dist/ApexUI-Kit/templates/MeteorCard/MeteorCard.jsx +34 -45
  17. package/dist/ApexUI-Kit/templates/NavDropDown/NavDropDown.jsx +272 -101
  18. package/dist/ApexUI-Kit/templates/NavMenu/NavMenu.jsx +8 -2
  19. package/dist/ApexUI-Kit/templates/ParallaxCard/ParallaxCard.jsx +13 -11
  20. package/dist/ApexUI-Kit/templates/ParallaxCarousel/ParallaxCarousel.jsx +9 -9
  21. package/dist/ApexUI-Kit/templates/ParallaxProfile/ParallaxProfile.jsx +41 -38
  22. package/dist/ApexUI-Kit/templates/PointerFollower/PointerFollower.jsx +7 -7
  23. package/dist/ApexUI-Kit/templates/SmartGridCard/SmartGridCard.jsx +4 -3
  24. package/dist/ApexUI-Kit/templates/SmokeBackground/SmokeBackground.jsx +13 -2
  25. package/dist/ApexUI-Kit/templates/Testimonials/Testimonials.jsx +11 -8
  26. package/dist/ApexUI-Kit/templates/ThemeToggle/ThemeAnimation.js +40 -69
  27. package/dist/ApexUI-Kit/templates/ThemeToggle/ThemeToggle.css +1 -1
  28. package/dist/ApexUI-Kit/templates/ThemeToggle/ThemeToggle.jsx +25 -7
  29. package/dist/ApexUI-Kit/templates/ToolTip/ToolTip.jsx +71 -52
  30. package/dist/cli/index.js +24 -13
  31. package/package.json +2 -2
  32. package/dist/ApexUI-Kit/templates/WaterDropReveal/WaterDropReveal.jsx +0 -151
@@ -1,114 +1,285 @@
1
- import React, { useState, useRef, useEffect } from 'react';
2
- import { gsap } from 'gsap';
3
- import { ChevronDown } from 'lucide-react';
4
- const DropdownPanel = ({ item, isActive }) => {
5
- const panelRef = useRef(null);
1
+ import React, { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from 'react';
2
+ import { AnimatePresence, LayoutGroup, MotionConfig, motion } from 'framer-motion';
3
+ import { ArrowRight, ArrowUpRight, ChevronDown } from 'lucide-react';
6
4
 
7
- // GSAP animation for revealing the panel
8
- useEffect(() => {
9
- if (isActive) {
10
- gsap.to(panelRef.current, {
11
- autoAlpha: 1,
12
- y: 0,
13
- scale: 1,
14
- duration: 0.2,
15
- ease: 'power3.out'
16
- });
17
- } else {
18
- gsap.to(panelRef.current, {
19
- autoAlpha: 0,
20
- y: -10,
21
- scale: 0.95,
22
- duration: 0.2,
23
- ease: 'power3.in'
24
- });
25
- }
26
- }, [isActive]);
5
+ const ease = [0.22, 1, 0.36, 1];
6
+ const CLOSE_DELAY = 140;
7
+
8
+ /* "services": two columns of feature tiles with a monogram (or a custom icon). */
9
+ const Services = ({ items }) => (
10
+ <ul className="grid w-[min(34rem,calc(100vw-2rem))] grid-cols-1 gap-1 p-2 sm:grid-cols-2">
11
+ {items.map((sub, i) => (
12
+ <li key={i}>
13
+ <a href={sub.href} className="group flex items-start gap-3 rounded-xl p-3 outline-none transition-colors hover:bg-white/[0.05] focus-visible:bg-white/[0.07]">
14
+ <span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border border-lime-400/20 bg-lime-400/10 text-sm font-semibold text-lime-300 transition-colors group-hover:border-lime-400/40 group-hover:bg-lime-400/15">
15
+ {sub.icon ? <sub.icon size={16} /> : sub.label.charAt(0)}
16
+ </span>
17
+ <span className="min-w-0">
18
+ <span className="flex items-center gap-1 text-sm font-medium text-zinc-100">
19
+ {sub.label}
20
+ <ArrowRight size={13} className="-translate-x-1 text-lime-300 opacity-0 transition-all duration-200 group-hover:translate-x-0 group-hover:opacity-100" />
21
+ </span>
22
+ {sub.subtitle && <span className="mt-0.5 block text-[0.8rem] leading-snug text-zinc-500">{sub.subtitle}</span>}
23
+ </span>
24
+ </a>
25
+ </li>
26
+ ))}
27
+ </ul>
28
+ );
29
+
30
+ /* "products": image cards, four across on wide screens. */
31
+ const Products = ({ items }) => (
32
+ <ul className="grid w-[min(40rem,calc(100vw-2rem))] grid-cols-2 gap-2 p-2 sm:grid-cols-4">
33
+ {items.map((sub, i) => (
34
+ <li key={i}>
35
+ <a href={sub.href} className="group block rounded-xl p-1.5 outline-none transition-colors hover:bg-white/[0.05] focus-visible:bg-white/[0.07]">
36
+ <span className="block aspect-[4/3] overflow-hidden sm:aspect-square rounded-lg bg-zinc-900 ring-1 ring-white/10">
37
+ <img src={sub.image} alt="" decoding="async" className="h-full w-full object-cover transition-transform duration-500 group-hover:scale-110" />
38
+ </span>
39
+ <span className="mt-2 block truncate px-0.5 text-sm font-medium text-zinc-100">{sub.label}</span>
40
+ {sub.subtitle && <span className="block truncate px-0.5 text-xs text-zinc-500">{sub.subtitle}</span>}
41
+ </a>
42
+ </li>
43
+ ))}
44
+ </ul>
45
+ );
27
46
 
47
+ /* "links": a titled list; switches to two columns when it gets long. */
48
+ const Links = ({ title, items }) => {
49
+ const wide = items.length > 5;
28
50
  return (
29
- <div
30
- ref={panelRef}
31
- style={{ visibility: 'hidden', transformOrigin: 'top left' }}
32
- className="absolute top-full left-1/2 -translate-x-1/2 mt-8 w-max max-w-4xl"
33
- >
34
- <div className="bg-gray-950 rounded-4xl shadow-xl p-6 z-50 border border-dashed border-gray-700/40">
35
- {/* Links Dropdown */}
36
- {item.type === "links" && (
37
- <ul className="grid grid-cols-2 gap-x-8 gap-y-2 text-left">
38
- {item.submenu.map((subItem, i) => (
39
- <li key={i}>
40
- <a href={subItem.href} className="block p-1.5 text-sm text-gray-400 rounded-md transition-colors duration-75 hover:text-lime-400 hover:bg-slate-800">
41
- {subItem.label}
42
- </a>
43
- </li>
44
- ))}
45
- </ul>
46
- )}
47
- {/* Products Dropdown */}
48
- {item.type === "products" && (
49
- <div className=' border border-gray-700/40 bg-black/10 rounded-2xl p-5'>
50
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4 z-50">
51
- {item.submenu.map((subItem, i) => (
52
- <a href={subItem.href} key={i} className={`flex items-center gap-4 bg-slate-800/50 p-3 rounded-lg hover:bg-slate-800 transition-all group w-80`}>
53
- <div className="overflow-hidden rounded-md">
54
- <img src={subItem.image} alt={subItem.label} className="w-20 h-20 object-cover transition-transform duration-300 group-hover:scale-110" />
55
- </div>
56
- <div className="text-left">
57
- <h3 className="text-gray-200 font-medium text-sm">{subItem.label}</h3>
58
- <p className="text-gray-500 text-xs">{subItem.subtitle}</p>
59
- </div>
60
- </a>
61
- ))}
62
- </div>
63
- </div>
64
- )}
65
- {/* Services Dropdown */}
66
- {item.type === "services" && (
67
- <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
68
- {item.submenu.map((subItem, i) => (
69
- <a href={subItem.href} key={i} className="group block bg-slate-800/50 p-3 rounded-lg hover:bg-slate-800 transition-all w-64">
70
- <h3 className="text-lime-400 font-medium transition-colors group-hover:text-lime-300">{subItem.label}</h3>
71
- <p className="text-gray-500 text-sm">{subItem.subtitle}</p>
72
- </a>
73
- ))}
74
- </div>
75
- )}
76
- </div>
51
+ <div className={`p-2 ${wide ? 'w-[min(26rem,calc(100vw-2rem))]' : 'w-[min(15rem,calc(100vw-2rem))]'}`}>
52
+ <p className="px-3 pb-1 pt-2 text-[0.68rem] font-medium uppercase tracking-[0.14em] text-zinc-500">{title}</p>
53
+ <ul className={wide ? 'grid grid-cols-2' : ''}>
54
+ {items.map((sub, i) => (
55
+ <li key={i}>
56
+ <a href={sub.href} className="group flex items-center justify-between gap-3 rounded-lg px-3 py-2 text-sm text-zinc-400 outline-none transition-colors hover:bg-white/[0.05] hover:text-zinc-100 focus-visible:bg-white/[0.07] focus-visible:text-zinc-100">
57
+ {sub.label}
58
+ <ArrowUpRight size={14} className="text-lime-300 opacity-0 transition-opacity group-hover:opacity-100" />
59
+ </a>
60
+ </li>
61
+ ))}
62
+ </ul>
77
63
  </div>
78
64
  );
79
65
  };
80
66
 
81
- const NavDropDown = ({ navData }) => {
82
- const [openIndex, setOpenIndex] = useState(null);
67
+ const PanelContent = ({ item }) => {
68
+ if (item.type === 'products') return <Products items={item.submenu} />;
69
+ if (item.type === 'links') return <Links title={item.label} items={item.submenu} />;
70
+ return <Services items={item.submenu} />;
71
+ };
72
+
73
+ const NavDropDown = ({ navData = [], className = '' }) => {
74
+ const uid = useId();
75
+ const rootRef = useRef(null);
76
+ const barRef = useRef(null);
77
+ const triggerRefs = useRef([]);
78
+ const contentRef = useRef(null);
79
+ const closeTimer = useRef(0);
80
+ const pointerType = useRef('');
81
+
82
+ const [open, setOpen] = useState(null);
83
+ const [hovered, setHovered] = useState(null);
84
+ // +1 / -1: which side the next panel's content slides in from.
85
+ const [dir, setDir] = useState(0);
86
+ const [size, setSize] = useState({ w: 0, h: 0 });
87
+ const [caretX, setCaretX] = useState(0);
88
+
89
+ const show = useCallback((index) => {
90
+ clearTimeout(closeTimer.current);
91
+ setOpen((prev) => {
92
+ if (prev !== null && prev !== index) setDir(index > prev ? 1 : -1);
93
+ else if (prev === null) setDir(0);
94
+ return index;
95
+ });
96
+ }, []);
97
+
98
+ const hide = useCallback(() => {
99
+ clearTimeout(closeTimer.current);
100
+ setOpen(null);
101
+ }, []);
102
+
103
+ // A short delay lets the pointer travel from a trigger into the panel.
104
+ const hideSoon = () => {
105
+ clearTimeout(closeTimer.current);
106
+ closeTimer.current = setTimeout(hide, CLOSE_DELAY);
107
+ };
108
+
109
+ // Size the panel to its content and point the caret at the open trigger.
110
+ const measure = useCallback(() => {
111
+ const content = contentRef.current;
112
+ const bar = barRef.current;
113
+ const trigger = triggerRefs.current[open];
114
+ if (!content || !bar || !trigger) return;
115
+ const w = content.offsetWidth;
116
+ setSize({ w, h: content.offsetHeight });
117
+ const barBox = bar.getBoundingClientRect();
118
+ const t = trigger.getBoundingClientRect();
119
+ const panelLeft = (barBox.width - w) / 2;
120
+ setCaretX(Math.min(Math.max(t.left - barBox.left + t.width / 2 - panelLeft, 20), w - 20));
121
+ }, [open]);
122
+
123
+ useLayoutEffect(() => {
124
+ if (open !== null) measure();
125
+ }, [open, measure]);
126
+
127
+ useEffect(() => {
128
+ if (open === null) return undefined;
129
+ const onKey = (e) => {
130
+ if (e.key !== 'Escape') return;
131
+ triggerRefs.current[open]?.focus();
132
+ hide();
133
+ };
134
+ const onPointer = (e) => {
135
+ if (!rootRef.current?.contains(e.target)) hide();
136
+ };
137
+ window.addEventListener('keydown', onKey);
138
+ window.addEventListener('pointerdown', onPointer);
139
+ window.addEventListener('resize', measure);
140
+ return () => {
141
+ window.removeEventListener('keydown', onKey);
142
+ window.removeEventListener('pointerdown', onPointer);
143
+ window.removeEventListener('resize', measure);
144
+ };
145
+ }, [open, hide, measure]);
146
+
147
+ useEffect(() => () => clearTimeout(closeTimer.current), []);
148
+
149
+ // Warm the cache for submenu images so a panel never opens with empty tiles.
150
+ useEffect(() => {
151
+ const id = setTimeout(() => {
152
+ navData.forEach((item) => item.submenu?.forEach((sub) => {
153
+ if (sub.image) new Image().src = sub.image;
154
+ }));
155
+ }, 300);
156
+ return () => clearTimeout(id);
157
+ }, [navData]);
158
+
159
+ const onTriggerKey = (e, index) => {
160
+ if (e.key !== 'ArrowDown') return;
161
+ e.preventDefault();
162
+ show(index);
163
+ // Wait for the panel to render, then move focus into it.
164
+ requestAnimationFrame(() => contentRef.current?.querySelector('a')?.focus());
165
+ };
166
+
167
+ const active = open !== null ? navData[open] : null;
168
+ const panelId = `${uid}-panel`;
83
169
 
84
170
  return (
85
- // New container panel with dashed border
86
- <div className=" inline-block bg-stone-950 border border-dashed border-slate-300/20 rounded-xl p-4 fixed" style={{ zIndex: '4000' }}>
87
- <nav className="relative">
88
- <ul className="flex space-x-2 justify-center">
89
- {navData.map((item, index) => (
90
- <li
91
- key={index}
92
- className="relative"
93
- onMouseEnter={() => item.submenu && setOpenIndex(index)}
94
- onMouseLeave={() => item.submenu && setOpenIndex(null)}
95
- >
96
- <a
97
- href={item.href || '#'}
98
- className="flex items-center px-3 py-2 text-sm font-medium hover:text-lime-400 text-slate-300 transition-colors duration-300 cursor-pointer"
171
+ <MotionConfig reducedMotion="user">
172
+ <div
173
+ ref={rootRef}
174
+ className={`relative inline-block max-w-full ${className}`}
175
+ style={{ zIndex: 4000 }}
176
+ onPointerLeave={(e) => e.pointerType === 'mouse' && hideSoon()}
177
+ onPointerEnter={() => clearTimeout(closeTimer.current)}
178
+ onBlur={(e) => !rootRef.current?.contains(e.relatedTarget) && hide()}
179
+ >
180
+ <nav aria-label="Main" ref={barRef} className="max-w-full overflow-x-auto rounded-full border border-white/10 bg-zinc-950/85 p-1 [scrollbar-width:none] sm:p-1.5 shadow-[0_10px_40px_-12px_rgba(0,0,0,0.7)] backdrop-blur-xl">
181
+ <LayoutGroup id={uid}>
182
+ <ul className="flex items-center gap-0.5" onPointerLeave={() => setHovered(null)}>
183
+ {navData.map((item, index) => {
184
+ const hasMenu = Boolean(item.submenu?.length);
185
+ const isOpen = open === index;
186
+ const lit = hovered === index || (hovered === null && isOpen);
187
+ const base = `relative flex items-center gap-0.5 whitespace-nowrap rounded-full px-2 py-1.5 text-[0.78rem] font-medium sm:gap-1 sm:px-3.5 sm:py-2 sm:text-sm outline-none transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-lime-400/60 ${isOpen ? 'text-lime-300' : 'text-zinc-300 hover:text-white'}`;
188
+ const highlight = lit && (
189
+ <motion.span layoutId="hover" className="absolute inset-0 -z-10 rounded-full bg-white/[0.07]" transition={{ type: 'spring', stiffness: 500, damping: 40 }} />
190
+ );
191
+ return (
192
+ <li key={index} className="relative isolate" onPointerEnter={() => setHovered(index)}>
193
+ {hasMenu ? (
194
+ <button
195
+ type="button"
196
+ ref={(el) => (triggerRefs.current[index] = el)}
197
+ className={base}
198
+ aria-expanded={isOpen}
199
+ aria-controls={isOpen ? panelId : undefined}
200
+ onPointerEnter={(e) => e.pointerType === 'mouse' && show(index)}
201
+ onPointerDown={(e) => (pointerType.current = e.pointerType)}
202
+ onClick={() => {
203
+ // Hover already opened it for a mouse; touch and keyboard toggle.
204
+ if (pointerType.current === 'mouse' || !isOpen) show(index);
205
+ else hide();
206
+ pointerType.current = '';
207
+ }}
208
+ onKeyDown={(e) => onTriggerKey(e, index)}
209
+ >
210
+ {highlight}
211
+ {item.label}
212
+ <ChevronDown size={15} className={`transition-transform duration-300 ${isOpen ? 'rotate-180' : ''}`} />
213
+ </button>
214
+ ) : (
215
+ <a href={item.href || '#'} className={base} onPointerEnter={(e) => e.pointerType === 'mouse' && hideSoon()}>
216
+ {highlight}
217
+ {item.label}
218
+ </a>
219
+ )}
220
+ </li>
221
+ );
222
+ })}
223
+ </ul>
224
+ </LayoutGroup>
225
+ </nav>
226
+
227
+ {/* One shared panel that resizes and slides its content as you move between menus. */}
228
+ <div className="pointer-events-none absolute left-1/2 top-full flex -translate-x-1/2 justify-center pt-3">
229
+ <AnimatePresence>
230
+ {active && (
231
+ <motion.div
232
+ id={panelId}
233
+ className="pointer-events-auto relative origin-top"
234
+ initial={{ opacity: 0, y: -8, scale: 0.97 }}
235
+ animate={{ opacity: 1, y: 0, scale: 1 }}
236
+ exit={{ opacity: 0, y: -6, scale: 0.98, transition: { duration: 0.15 } }}
237
+ transition={{ duration: 0.25, ease }}
99
238
  >
100
- {item.label}
101
- {item.submenu && (
102
- <ChevronDown size={16} className={`ml-1 transition-transform duration-300 ${openIndex === index ? "rotate-180" : ""}`} />
103
- )}
104
- </a>
105
- {item.submenu && <DropdownPanel item={item} isActive={openIndex === index} />}
106
- </li>
107
- ))}
108
- </ul>
109
- </nav>
110
- </div>
239
+ <motion.span
240
+ aria-hidden
241
+ className="absolute -top-[5px] z-10 h-2.5 w-2.5 -translate-x-1/2 rotate-45 rounded-[2px] border-l border-t border-white/10 bg-zinc-950"
242
+ initial={false}
243
+ animate={{ left: caretX }}
244
+ transition={{ duration: 0.3, ease }}
245
+ />
246
+ <motion.div
247
+ className="relative overflow-hidden rounded-2xl border border-white/10 bg-zinc-950/95 shadow-[0_24px_60px_-20px_rgba(0,0,0,0.8)] backdrop-blur-xl"
248
+ initial={false}
249
+ animate={{ width: size.w || 'auto', height: size.h || 'auto' }}
250
+ transition={{ duration: 0.3, ease }}
251
+ >
252
+ <span aria-hidden className="pointer-events-none absolute inset-x-10 top-0 z-10 h-px bg-gradient-to-r from-transparent via-lime-400/50 to-transparent" />
253
+ <AnimatePresence initial={false} custom={dir}>
254
+ <motion.div
255
+ key={open}
256
+ // The leaving copy unmounts later; ignore its null so the ref keeps the new content.
257
+ ref={(el) => {
258
+ if (el) contentRef.current = el;
259
+ }}
260
+ custom={dir}
261
+ className="absolute left-0 top-0"
262
+ variants={{
263
+ enter: (d) => ({ opacity: 0, x: d * 32 }),
264
+ center: { opacity: 1, x: 0 },
265
+ leave: (d) => ({ opacity: 0, x: d * -32 }),
266
+ }}
267
+ initial="enter"
268
+ animate="center"
269
+ exit="leave"
270
+ transition={{ duration: 0.25, ease }}
271
+ >
272
+ <PanelContent item={active} />
273
+ </motion.div>
274
+ </AnimatePresence>
275
+ </motion.div>
276
+ </motion.div>
277
+ )}
278
+ </AnimatePresence>
279
+ </div>
280
+ </div>
281
+ </MotionConfig>
111
282
  );
112
283
  };
113
284
 
114
- export default NavDropDown;
285
+ export default NavDropDown;
@@ -121,7 +121,13 @@ const ResponsiveNavMenu = (props) => {
121
121
  </div>
122
122
 
123
123
  <div className="md:hidden flex w-full justify-end">
124
- <button onClick={() => setIsOpen(!isOpen)} className="text-white focus:outline-none">
124
+ {/* Same backdrop as the desktop bar, so the icon stays visible on light pages too. */}
125
+ <button
126
+ onClick={() => setIsOpen(!isOpen)}
127
+ aria-label={isOpen ? "Close menu" : "Open menu"}
128
+ className="rounded-full border border-white/10 p-2.5 text-white shadow-lg focus:outline-none"
129
+ style={{ backdropFilter: 'blur(16px)', WebkitBackdropFilter: 'blur(16px)', background: props.backgroundColor ?? 'rgba(17, 24, 39, 0.7)' }}
130
+ >
125
131
  <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
126
132
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={isOpen ? "M6 18L18 6M6 6l12 12" : "M4 6h16M4 12h16m-7 6h7"} />
127
133
  </svg>
@@ -132,7 +138,7 @@ const ResponsiveNavMenu = (props) => {
132
138
  <div className="md:hidden absolute top-20 left-4 right-4 rounded-xl shadow-lg border border-white/10"
133
139
  style={{ backdropFilter: 'blur(16px)', WebkitBackdropFilter: 'blur(16px)', background: 'rgba(17, 24, 39, 0.7)' }}>
134
140
  <div className="flex flex-col items-center gap-2 p-4">
135
- {navItems.map((label, i) => (
141
+ {navItems.map((label) => (
136
142
  <button
137
143
  key={label}
138
144
  onClick={() => {
@@ -14,6 +14,7 @@ const ParallaxCard = ({ cardData = [], tiltEnable, glareEnable, scale, perspecti
14
14
  const linkRefs = useRef([]);
15
15
 
16
16
  useEffect(() => {
17
+ const cleanups = [];
17
18
  cardData.forEach((_, idx) => {
18
19
  const cardElement = cardRefs.current[idx];
19
20
  const titleElement = titleRefs.current[idx];
@@ -40,11 +41,11 @@ const ParallaxCard = ({ cardData = [], tiltEnable, glareEnable, scale, perspecti
40
41
  ease: "power3.out",
41
42
  });
42
43
 
43
- gsap.to(titleElement, { duration: 0.7, x: rotateY * 0.8, y: -rotateX * 0.8 });
44
- gsap.to(imageElement, { duration: 0.8, x: rotateY * 1.5, y: -rotateX * 1.5 });
45
- gsap.to(descElement, { duration: 0.9, x: rotateY * 1.2, y: -rotateX * 1.2 });
46
- gsap.to(buttonElement, { duration: 1, x: rotateY * 1.1, y: -rotateX * 1.1 });
47
- gsap.to(linkElement, { duration: 1, x: rotateY * 1.1, y: -rotateX * 1.1 });
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 });
48
49
  };
49
50
 
50
51
  const handleMouseLeave = () => {
@@ -56,7 +57,7 @@ const ParallaxCard = ({ cardData = [], tiltEnable, glareEnable, scale, perspecti
56
57
  scale: 1,
57
58
  ease: "elastic.out(1, 0.5)",
58
59
  });
59
- gsap.to([titleElement, imageElement, descElement, buttonElement, linkElement], {
60
+ gsap.to([titleElement, imageElement, descElement, buttonElement, linkElement].filter(Boolean), {
60
61
  duration: 1.2,
61
62
  x: 0,
62
63
  y: 0,
@@ -65,18 +66,19 @@ const ParallaxCard = ({ cardData = [], tiltEnable, glareEnable, scale, perspecti
65
66
  }
66
67
  };
67
68
 
68
- cardElement.addEventListener("mousemove", handleMouseMove);
69
+ cardElement.addEventListener("mousemove", handleMouseMove, { passive: true });
69
70
  cardElement.addEventListener("mouseleave", handleMouseLeave);
70
71
 
71
- return () => {
72
+ cleanups.push(() => {
72
73
  cardElement.removeEventListener("mousemove", handleMouseMove);
73
74
  cardElement.removeEventListener("mouseleave", handleMouseLeave);
74
- };
75
+ });
75
76
  });
77
+ return () => cleanups.forEach((fn) => fn());
76
78
  }, [cardData, tiltEnable, scale]);
77
79
 
78
80
  return (
79
- <div className="flex flex-wrap gap-8 justify-center">
81
+ <div className="flex w-full flex-wrap gap-8 justify-center">
80
82
  {cardData.map((card, idx) => (
81
83
  <div
82
84
  key={idx}
@@ -84,7 +86,7 @@ const ParallaxCard = ({ cardData = [], tiltEnable, glareEnable, scale, perspecti
84
86
  className="w-full max-w-md rounded-2xl"
85
87
  style={{ transformStyle: 'preserve-3d', perspective: `${perspective}px` }}
86
88
  >
87
- <div className="relative md:w-full w-70 h-[50vh] md:h-full bg-stone-950 rounded-2xl p-8 border border-lime-500/20 shadow-2 shadow-lime-900/40 overflow-hidden"
89
+ <div className="relative w-full md:h-full bg-stone-950 rounded-2xl p-6 sm:p-8 border border-lime-500/20 shadow-2 shadow-lime-900/40 overflow-hidden"
88
90
  style={{ transformStyle: 'preserve-3d' }}
89
91
  >
90
92
  {glareEnable && (
@@ -47,10 +47,10 @@ const CarouselCard = ({ slide, isCenter, isScrolling }) => {
47
47
  <img src={slide.imageUrl} alt={slide.title} className="w-full h-full object-cover group-hover:opacity-30 transition-all duration-300 ease-in-out rounded-2xl" />
48
48
  <img src={slide.pngUrl} alt={slide.title} className="absolute inset-0 w-full h-full object-cover group-hover:translate-z-15 transition-all duration-300 ease-in-out rounded-2xl" />
49
49
  </div>
50
- <div className="relative flex flex-col justify-end h-full text-white p-8" style={{ transformStyle: 'preserve-3d', perspective: '1500px' }}>
51
- <h2 className="text-3xl font-bold group-hover:translate-z-18 transition-all duration-300 ease-in-out">{slide.title}</h2>
50
+ <div className="relative flex flex-col justify-end h-full text-white p-5 sm:p-8" style={{ transformStyle: 'preserve-3d', perspective: '1500px' }}>
51
+ <h2 className="text-2xl sm:text-3xl font-bold group-hover:translate-z-18 transition-all duration-300 ease-in-out">{slide.title}</h2>
52
52
  <p className="text-lime-400 mt-1 group-hover:translate-z-18 transition-all duration-300 ease-in-out">{slide.subtitle}</p>
53
- <p className="text-slate-300 mt-4 text-sm leading-relaxed group-hover:translate-z-18 transition-all duration-300 ease-in-out">{slide.text}</p>
53
+ <p className="text-slate-300 mt-3 sm:mt-4 text-xs sm:text-sm leading-relaxed line-clamp-3 sm:line-clamp-none group-hover:translate-z-18 transition-all duration-300 ease-in-out">{slide.text}</p>
54
54
  </div>
55
55
  </motion.div>
56
56
  );
@@ -109,7 +109,7 @@ const ParallaxCarousel = ({ slides }) => {
109
109
  return (
110
110
  <div className="relative w-full flex flex-col items-center px-2">
111
111
  <div
112
- className="relative w-full h-[480px] portrait:h-[480px] portrait:px-0"
112
+ className="relative w-full h-[380px] sm:h-[480px]"
113
113
  style={{ perspective: '1200px', transformStyle: 'preserve-3d' }}
114
114
  >
115
115
  {slides.map((slide, index) => {
@@ -125,7 +125,7 @@ const ParallaxCarousel = ({ slides }) => {
125
125
  return (
126
126
  <motion.div
127
127
  key={index}
128
- className={`absolute w-full h-[340px] portrait:w-full portrait:h-[480px] top-0 left-0 portrait:left-0 portrait:top-0 ${positionOffset === 0 ? '' : ' pointer-events-none'} md:w-[50%] md:h-[400px] md:top-[50px] md:left-1/2 lg:-translate-x-1/2`}
128
+ className={`absolute left-1/2 top-1/2 aspect-[25/26] w-[min(80%,25rem)] sm:w-[min(88%,25rem)] -translate-x-1/2 -translate-y-1/2 ${positionOffset === 0 ? '' : 'pointer-events-none'}`}
129
129
  initial={false}
130
130
  style={{ transformStyle: 'preserve-3d', willChange: 'transform, opacity' }}
131
131
  animate={{
@@ -141,11 +141,11 @@ const ParallaxCarousel = ({ slides }) => {
141
141
  })}
142
142
  </div>
143
143
 
144
- <motion.button onClick={handlePrev} className="absolute -left-10 lg:left-0 top-1/2 -translate-y-1/2 z-30 p-3 rounded-full text-slate-400 hover:text-lime-400 transition-colors" whileTap={{ scale: 0.9 }}>
145
- <ArrowLeft size={32} />
144
+ <motion.button onClick={handlePrev} className="absolute left-0 top-1/2 -translate-y-1/2 z-30 p-1.5 sm:p-3 rounded-full text-slate-400 hover:text-lime-400 transition-colors" whileTap={{ scale: 0.9 }}>
145
+ <ArrowLeft size={32} className="h-5 w-5 sm:h-8 sm:w-8" />
146
146
  </motion.button>
147
- <motion.button onClick={handleNext} className="absolute -right-10 lg:right-0 top-1/2 -translate-y-1/2 z-30 p-3 rounded-full text-slate-400 hover:text-lime-400 transition-colors" whileTap={{ scale: 0.9 }}>
148
- <ArrowRight size={32} />
147
+ <motion.button onClick={handleNext} className="absolute right-0 top-1/2 -translate-y-1/2 z-30 p-1.5 sm:p-3 rounded-full text-slate-400 hover:text-lime-400 transition-colors" whileTap={{ scale: 0.9 }}>
148
+ <ArrowRight size={32} className="h-5 w-5 sm:h-8 sm:w-8" />
149
149
  </motion.button>
150
150
  </div>
151
151
  );