apex-ui-kit 2.0.6 → 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.
- 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 +129 -0
- package/dist/ApexUI-Kit/templates/Cards/BasicCard.jsx +3 -3
- package/dist/ApexUI-Kit/templates/Carousel/ParallaxCarousel.jsx +0 -161
- package/dist/ApexUI-Kit/templates/DragCarousel/DragCarousel.jsx +39 -7
- package/dist/ApexUI-Kit/templates/EdgeGlowForm/EdgeGlowFrom.jsx +116 -98
- package/dist/ApexUI-Kit/templates/FlipProfile/FlipProfile.jsx +69 -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/ParallaxCard/ParallaxCard.jsx +13 -11
- package/dist/ApexUI-Kit/templates/ParallaxCarousel/ParallaxCarousel.jsx +60 -67
- package/dist/ApexUI-Kit/templates/ParallaxProfile/ParallaxProfile.jsx +69 -55
- package/dist/ApexUI-Kit/templates/PointerFollower/PointerFollower.jsx +34 -14
- package/dist/ApexUI-Kit/templates/ScrollMarquee/CardMarquee.jsx +0 -0
- package/dist/ApexUI-Kit/templates/SmartGridCard/SmartGridCard.jsx +4 -3
- package/dist/ApexUI-Kit/templates/SmokeBackground/SmokeBackground.jsx +35 -16
- 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 +5 -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 +25 -14
- package/package.json +2 -2
- package/dist/ApexUI-Kit/templates/GlassCardStack/GlassCardStack.jsx +0 -111
- package/dist/ApexUI-Kit/templates/WaterDropReveal/WaterDropReveal.jsx +0 -151
|
@@ -26,66 +26,87 @@ const AccordionMarquee = ({bgColor='#bbf451', textColor='#27272a'}) => {
|
|
|
26
26
|
const containerRef = useRef(null);
|
|
27
27
|
|
|
28
28
|
useLayoutEffect(() => {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
29
|
+
const container = containerRef.current;
|
|
30
|
+
if (!container) return undefined;
|
|
31
|
+
const cleanups = [];
|
|
32
|
+
|
|
33
|
+
// Scoped to this instance and reverted on unmount, so a remount (or React
|
|
34
|
+
// StrictMode) no longer stacks a second set of handlers and tweens — that
|
|
35
|
+
// is what left several marquee bands visible at once.
|
|
36
|
+
const ctx = gsap.context(() => {
|
|
37
|
+
const items = gsap.utils.toArray(".accordion-item", container);
|
|
38
|
+
let activeItem = null;
|
|
39
|
+
let activeMarquee = null;
|
|
40
|
+
|
|
41
|
+
items.forEach((item) => {
|
|
42
|
+
const marqueeText = item.querySelector(".marquee-text");
|
|
43
|
+
const title = item.querySelector(".accordion-title");
|
|
44
|
+
|
|
45
|
+
// Marquee text ko 2 baar duplicate karte hain for seamless loop
|
|
46
|
+
// (the original text is kept so a remount doesn't duplicate it again)
|
|
47
|
+
if (!marqueeText.dataset.text) marqueeText.dataset.text = marqueeText.innerText;
|
|
48
|
+
const marqueeContent = marqueeText.dataset.text;
|
|
49
|
+
const sparkleIconSVG = `<svg xmlns="http://www.w3.org/2000/svg" width="100" height="70" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-sparkle-icon lucide-sparkle"><path d="M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z" class="sparkle-icon"/></svg>`;
|
|
50
|
+
marqueeText.innerHTML = [marqueeContent, sparkleIconSVG, marqueeContent, sparkleIconSVG, marqueeContent, sparkleIconSVG, marqueeContent, sparkleIconSVG].join("");
|
|
51
|
+
|
|
52
|
+
// Seamless Marquee animation
|
|
53
|
+
const marqueeWidth = marqueeText.scrollWidth / 2;
|
|
54
|
+
const marqueeTl = gsap.to(marqueeText, {
|
|
55
|
+
x: -marqueeWidth,
|
|
56
|
+
duration: marqueeWidth / 30, // Speed ko consistent rakhta hai
|
|
57
|
+
ease: "linear",
|
|
58
|
+
repeat: -1,
|
|
59
|
+
}).pause();
|
|
60
|
+
|
|
61
|
+
// overwrite: true kills a still-delayed tween on the same element,
|
|
62
|
+
// so fast pointer moves can't leave a title and a band both visible.
|
|
63
|
+
const onEnter = () => {
|
|
64
|
+
if (activeItem === item) return;
|
|
65
|
+
|
|
66
|
+
if (activeItem) {
|
|
67
|
+
const oldTitle = activeItem.querySelector(".accordion-title");
|
|
68
|
+
const oldMarqueeWrapper = activeItem.querySelector(".marquee-wrapper");
|
|
69
|
+
gsap.to(oldTitle, { autoAlpha: 1, duration: 0.2, ease: "power3.inOut", overwrite: true });
|
|
70
|
+
gsap.to(oldMarqueeWrapper, { autoAlpha: 0, duration: 0.2, ease: "power3.inOut", overwrite: true });
|
|
71
|
+
if (activeMarquee) activeMarquee.pause();
|
|
72
|
+
}
|
|
61
73
|
|
|
62
|
-
|
|
63
|
-
|
|
74
|
+
activeItem = item;
|
|
75
|
+
activeMarquee = marqueeTl;
|
|
64
76
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
77
|
+
const marqueeWrapper = item.querySelector(".marquee-wrapper");
|
|
78
|
+
gsap.to(title, { autoAlpha: 0, duration: 0.2, ease: "power3.inOut", overwrite: true });
|
|
79
|
+
gsap.to(marqueeWrapper, { autoAlpha: 1, duration: 0.2, delay: 0.1, ease: "power3.inOut", overwrite: true });
|
|
68
80
|
|
|
69
|
-
|
|
81
|
+
marqueeTl.play();
|
|
82
|
+
};
|
|
83
|
+
item.addEventListener("mouseenter", onEnter);
|
|
84
|
+
cleanups.push(() => item.removeEventListener("mouseenter", onEnter));
|
|
70
85
|
});
|
|
71
|
-
});
|
|
72
86
|
|
|
73
|
-
|
|
74
|
-
|
|
87
|
+
const onLeave = () => {
|
|
88
|
+
if (!activeItem) return;
|
|
75
89
|
|
|
76
|
-
|
|
77
|
-
|
|
90
|
+
const activeTitle = activeItem.querySelector(".accordion-title");
|
|
91
|
+
const activeMarqueeWrapper = activeItem.querySelector(".marquee-wrapper");
|
|
78
92
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (activeMarquee) {
|
|
83
|
-
activeMarquee.pause();
|
|
84
|
-
}
|
|
85
|
-
activeItem = null;
|
|
86
|
-
activeMarquee = null;
|
|
87
|
-
});
|
|
93
|
+
gsap.to(activeTitle, { autoAlpha: 1, duration: 0.2, delay: 0.1, ease: "power3.inOut", overwrite: true });
|
|
94
|
+
gsap.to(activeMarqueeWrapper, { autoAlpha: 0, duration: 0.2, ease: "power3.inOut", overwrite: true });
|
|
88
95
|
|
|
96
|
+
if (activeMarquee) {
|
|
97
|
+
activeMarquee.pause();
|
|
98
|
+
}
|
|
99
|
+
activeItem = null;
|
|
100
|
+
activeMarquee = null;
|
|
101
|
+
};
|
|
102
|
+
container.addEventListener("mouseleave", onLeave);
|
|
103
|
+
cleanups.push(() => container.removeEventListener("mouseleave", onLeave));
|
|
104
|
+
}, container);
|
|
105
|
+
|
|
106
|
+
return () => {
|
|
107
|
+
cleanups.forEach((fn) => fn());
|
|
108
|
+
ctx.revert();
|
|
109
|
+
};
|
|
89
110
|
}, []);
|
|
90
111
|
|
|
91
112
|
return (
|
|
@@ -107,7 +128,7 @@ const AccordionMarquee = ({bgColor='#bbf451', textColor='#27272a'}) => {
|
|
|
107
128
|
}
|
|
108
129
|
`}
|
|
109
130
|
</style>
|
|
110
|
-
<div className="flex items-center justify-center w-
|
|
131
|
+
<div className="flex items-center justify-center w-full bg-gray-900 text-white font-sans">
|
|
111
132
|
<div ref={containerRef} className="flex flex-col w-full h-[80vh] md:h-[70vh]">
|
|
112
133
|
{accordionData.map((item, index) => (
|
|
113
134
|
<React.Fragment key={index}>
|
|
@@ -43,21 +43,23 @@ const BeamBar = ({
|
|
|
43
43
|
|
|
44
44
|
// Animate beam opacity gradually during width expansion
|
|
45
45
|
let start = null;
|
|
46
|
+
let frame = 0;
|
|
46
47
|
function animateBeam(ts) {
|
|
47
48
|
if (!start) start = ts;
|
|
48
49
|
const elapsed = (ts - start) / 1000;
|
|
49
50
|
const progress = Math.min(elapsed / duration, 1);
|
|
50
51
|
setBeamOpacity(0.2 + 0.8 * progress); // from 0.2 to 1.0
|
|
51
52
|
if (progress < 1) {
|
|
52
|
-
requestAnimationFrame(animateBeam);
|
|
53
|
+
frame = requestAnimationFrame(animateBeam);
|
|
53
54
|
} else {
|
|
54
55
|
setPulse(true);
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
|
-
requestAnimationFrame(animateBeam);
|
|
58
|
+
frame = requestAnimationFrame(animateBeam);
|
|
58
59
|
|
|
59
|
-
// Cleanup
|
|
60
|
+
// Cleanup (stop the running loop so a resize or unmount doesn't leave it going)
|
|
60
61
|
return () => {
|
|
62
|
+
cancelAnimationFrame(frame);
|
|
61
63
|
setBeamOpacity(0.2);
|
|
62
64
|
setPulse(false);
|
|
63
65
|
};
|
|
@@ -82,10 +82,22 @@ function BluePrintBackground({ color = '#9ae600', borderColor = 'rgb(98, 116, 14
|
|
|
82
82
|
}
|
|
83
83
|
};
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
// Hit-test at most once per frame instead of on every mousemove event.
|
|
86
|
+
let moveFrame = 0;
|
|
87
|
+
let lastMove = null;
|
|
88
|
+
const onMouseMove = (e) => {
|
|
89
|
+
lastMove = e;
|
|
90
|
+
if (moveFrame) return;
|
|
91
|
+
moveFrame = requestAnimationFrame(() => {
|
|
92
|
+
moveFrame = 0;
|
|
93
|
+
handleMouseMove(lastMove);
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
window.addEventListener("mousemove", onMouseMove, { passive: true });
|
|
86
97
|
|
|
87
98
|
return () => {
|
|
88
|
-
window.removeEventListener("mousemove",
|
|
99
|
+
window.removeEventListener("mousemove", onMouseMove);
|
|
100
|
+
cancelAnimationFrame(moveFrame);
|
|
89
101
|
if (scrollTween && typeof scrollTween.kill === "function") {
|
|
90
102
|
try { scrollTween.kill(); } catch { /* ignore */ }
|
|
91
103
|
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import React, { useState, useEffect, useRef } from "react";
|
|
2
|
+
import { gsap } from "gsap";
|
|
3
|
+
import { FaArrowRight, FaGithub, FaRegImage } from "react-icons/fa";
|
|
4
|
+
// Cards scale from the bottom, so the offset must beat the shrink (6% of the height) to peek out.
|
|
5
|
+
const CARD_STACK_OFFSET = 40;
|
|
6
|
+
const CARD_SCALE_STEP = 0.06;
|
|
7
|
+
const AUTO_CYCLE_INTERVAL = 5000;
|
|
8
|
+
export default function CardStack({
|
|
9
|
+
cards = [],
|
|
10
|
+
autoCycle = true,
|
|
11
|
+
cycleInterval = AUTO_CYCLE_INTERVAL,
|
|
12
|
+
}) {
|
|
13
|
+
const [active, setActive] = useState(0);
|
|
14
|
+
const [isHovered, setIsHovered] = useState(false);
|
|
15
|
+
const [isScrolling, setIsScrolling] = useState(false);
|
|
16
|
+
const timeoutRef = useRef(null);
|
|
17
|
+
const scrollTimeoutRef = useRef(null);
|
|
18
|
+
const containerRef = useRef(null);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!autoCycle || isHovered || isScrolling || cards.length < 2) return;
|
|
21
|
+
timeoutRef.current = setTimeout(() => {
|
|
22
|
+
setActive((prev) => (prev + 1) % cards.length);
|
|
23
|
+
}, cycleInterval);
|
|
24
|
+
return () => clearTimeout(timeoutRef.current);
|
|
25
|
+
}, [active, isHovered, isScrolling, autoCycle, cycleInterval, cards.length]);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const handleScroll = () => {
|
|
29
|
+
setIsScrolling(true);
|
|
30
|
+
clearTimeout(scrollTimeoutRef.current);
|
|
31
|
+
scrollTimeoutRef.current = setTimeout(() => setIsScrolling(false), 150);
|
|
32
|
+
};
|
|
33
|
+
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
34
|
+
return () => {
|
|
35
|
+
window.removeEventListener('scroll', handleScroll);
|
|
36
|
+
clearTimeout(scrollTimeoutRef.current);
|
|
37
|
+
};
|
|
38
|
+
}, []);
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (!containerRef.current) return;
|
|
41
|
+
const cardElements = gsap.utils.toArray(".card-item", containerRef.current);
|
|
42
|
+
const tl = gsap.timeline({
|
|
43
|
+
defaults: { duration: 0.7, ease: "elastic.out(1, 0.45)" },
|
|
44
|
+
});
|
|
45
|
+
cardElements.forEach((card, i) => {
|
|
46
|
+
tl.to(card, {
|
|
47
|
+
y: -i * CARD_STACK_OFFSET,
|
|
48
|
+
scale: 1 - i * CARD_SCALE_STEP,
|
|
49
|
+
// Back cards are dimmed instead of blurred: cheaper, and the images stay crisp.
|
|
50
|
+
filter: i === 0 ? "brightness(1)" : `brightness(${0.75 - i * 0.12})`,
|
|
51
|
+
opacity: 1,
|
|
52
|
+
zIndex: cards.length - i,
|
|
53
|
+
}, 0);
|
|
54
|
+
});
|
|
55
|
+
return () => {
|
|
56
|
+
tl.kill();
|
|
57
|
+
};
|
|
58
|
+
}, [active, cards.length]);
|
|
59
|
+
|
|
60
|
+
const next = () => setActive((prev) => (prev + 1) % cards.length);
|
|
61
|
+
const getCardIndex = (i) => (active + i) % cards.length;
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div
|
|
65
|
+
ref={containerRef}
|
|
66
|
+
className="relative flex justify-center items-end h-[440px] w-full max-w-lg mx-auto pb-2"
|
|
67
|
+
onMouseEnter={() => setIsHovered(true)}
|
|
68
|
+
onMouseLeave={() => setIsHovered(false)}
|
|
69
|
+
>
|
|
70
|
+
{cards.map((_, i) => {
|
|
71
|
+
if (i >= 4) return null;
|
|
72
|
+
|
|
73
|
+
const idx = getCardIndex(i);
|
|
74
|
+
const isTop = i === 0;
|
|
75
|
+
const card = cards[idx];
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div
|
|
79
|
+
key={idx}
|
|
80
|
+
onClick={isTop ? next : undefined}
|
|
81
|
+
className={`card-item group absolute bottom-2 left-0 right-0 mx-auto h-[360px] w-[280px] max-w-[calc(100%-1rem)] origin-bottom overflow-hidden rounded-[1.75rem] border border-white/15 bg-slate-950 shadow-[0_24px_60px_-20px_rgba(0,0,0,0.8)] select-none ${isTop ? "cursor-pointer" : "pointer-events-none"}`}
|
|
82
|
+
style={{ opacity: 0 }}
|
|
83
|
+
>
|
|
84
|
+
{/* Full-bleed image */}
|
|
85
|
+
{card.image ? (
|
|
86
|
+
<img src={card.image} alt={card.title} draggable="false" className="absolute inset-0 h-full w-full object-cover object-top transition-transform duration-700 group-hover:scale-105" />
|
|
87
|
+
) : (
|
|
88
|
+
<div className="absolute inset-0 flex items-center justify-center bg-slate-900"><FaRegImage className="text-4xl text-white/30" /></div>
|
|
89
|
+
)}
|
|
90
|
+
{/* Card colour tint + readable bottom */}
|
|
91
|
+
<div className={`absolute inset-0 bg-gradient-to-br ${card.color} opacity-40 mix-blend-soft-light`} />
|
|
92
|
+
<div className="absolute inset-x-0 bottom-0 h-3/4 bg-gradient-to-t from-black via-black/75 to-transparent" />
|
|
93
|
+
|
|
94
|
+
{/* Top row: subtitle chip + GitHub */}
|
|
95
|
+
<div className="absolute inset-x-0 top-0 flex items-start justify-between gap-2 p-4">
|
|
96
|
+
{card.subtitle && (
|
|
97
|
+
<span className="line-clamp-1 rounded-full border border-white/15 bg-black/40 px-3 py-1 text-[11px] font-medium text-white/85 backdrop-blur-md">{card.subtitle}</span>
|
|
98
|
+
)}
|
|
99
|
+
{card.github && (
|
|
100
|
+
<a href={card.github} target="_blank" rel="noopener noreferrer" onClick={(e) => e.stopPropagation()} aria-label="GitHub" className="ml-auto flex h-8 w-8 shrink-0 items-center justify-center rounded-full border border-white/15 bg-black/40 text-white/80 backdrop-blur-md transition hover:text-white">
|
|
101
|
+
<FaGithub className="text-base" />
|
|
102
|
+
</a>
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
{/* Bottom content */}
|
|
107
|
+
<div className="absolute inset-x-0 bottom-0 flex flex-col gap-1.5 p-5">
|
|
108
|
+
<h2 className="text-xl font-bold leading-tight tracking-tight text-white">{card.title}</h2>
|
|
109
|
+
{card.desc && <p className="line-clamp-2 text-[13px] leading-relaxed text-white/70">{card.desc}</p>}
|
|
110
|
+
<div className="mt-2 flex items-center justify-between gap-3">
|
|
111
|
+
{card.link ? (
|
|
112
|
+
<a href={card.link} target="_blank" rel="noopener noreferrer" onClick={(e) => e.stopPropagation()} className="inline-flex items-center gap-2 rounded-full bg-white px-4 py-1.5 text-xs font-semibold text-slate-950 transition hover:bg-white/85">
|
|
113
|
+
Visit <FaArrowRight className="text-[10px] transition-transform group-hover:translate-x-0.5" />
|
|
114
|
+
</a>
|
|
115
|
+
) : <span />}
|
|
116
|
+
{/* Progress dots */}
|
|
117
|
+
<div className="flex items-center gap-1.5" aria-hidden="true">
|
|
118
|
+
{cards.map((__, d) => (
|
|
119
|
+
<span key={d} className={`h-1.5 rounded-full transition-all duration-300 ${d === idx ? "w-4 bg-white" : "w-1.5 bg-white/35"}`} />
|
|
120
|
+
))}
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
})}
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
@@ -9,10 +9,10 @@ const ProfileCard = ({ title, summary, image, link }) => {
|
|
|
9
9
|
initial={{ y: 20, opacity: 0 }}
|
|
10
10
|
animate={{ y: 0, opacity: 1 }}
|
|
11
11
|
transition={{ type: "spring", stiffness: 300, damping: 20 }}
|
|
12
|
-
className="w-
|
|
12
|
+
className="w-[280px] max-w-full rounded-2xl bg-slate-950 border border-gray-800 shadow-2xl overflow-hidden p-2 shadow-lime-700/20"
|
|
13
13
|
>
|
|
14
|
-
{/* Image */}
|
|
15
|
-
<div className="h-[
|
|
14
|
+
{/* Image: fixed px size so the card keeps its shape on every screen */}
|
|
15
|
+
<div className="h-[200px] w-full rounded-b-4xl rounded-t-lg overflow-hidden">
|
|
16
16
|
<motion.img
|
|
17
17
|
src={image}
|
|
18
18
|
alt={title}
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
/* eslint-disable no-unused-vars */
|
|
2
|
-
import React, { useState, useEffect, useRef } from 'react';
|
|
3
|
-
import { motion, useMotionValue, useTransform } from 'framer-motion';
|
|
4
|
-
import { ArrowLeft, ArrowRight } from 'lucide-react';
|
|
5
|
-
|
|
6
|
-
// --- Card Component with Hover Parallax Effect ---
|
|
7
|
-
const CarouselCard = ({ slide, isCenter }) => {
|
|
8
|
-
const cardRef = useRef(null);
|
|
9
|
-
const x = useMotionValue(0);
|
|
10
|
-
const y = useMotionValue(0);
|
|
11
|
-
|
|
12
|
-
// Mouse move ke hisab se rotation calculate karta hai (effect ko strong banaya gaya hai)
|
|
13
|
-
const rotateX = useTransform(y, [-200, 200], [15, -15]);
|
|
14
|
-
const rotateY = useTransform(x, [-200, 200], [-15, 15]);
|
|
15
|
-
const handleMouseMove = (event) => {
|
|
16
|
-
if (!cardRef.current || !isCenter) return;
|
|
17
|
-
const rect = cardRef.current.getBoundingClientRect();
|
|
18
|
-
x.set(event.clientX - rect.left - rect.width / 2);
|
|
19
|
-
y.set(event.clientY - rect.top - rect.height / 2);
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const handleMouseLeave = () => {
|
|
23
|
-
x.set(0);
|
|
24
|
-
y.set(0);
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
return (
|
|
28
|
-
<motion.div
|
|
29
|
-
ref={cardRef}
|
|
30
|
-
className="w-full h-full bg-slate-900 rounded-2xl group"
|
|
31
|
-
style={{
|
|
32
|
-
transformStyle: 'preserve-3d',
|
|
33
|
-
perspective: '1500px',
|
|
34
|
-
rotateX: isCenter ? rotateX : 0,
|
|
35
|
-
rotateY: isCenter ? rotateY : 0,
|
|
36
|
-
}}
|
|
37
|
-
onMouseMove={handleMouseMove}
|
|
38
|
-
onMouseLeave={handleMouseLeave}
|
|
39
|
-
|
|
40
|
-
>
|
|
41
|
-
{/* Slide Content */}
|
|
42
|
-
<div className="absolute inset-0 w-full h-full"
|
|
43
|
-
style={{
|
|
44
|
-
transformStyle: 'preserve-3d',
|
|
45
|
-
perspective: '1500px',
|
|
46
|
-
}}
|
|
47
|
-
>
|
|
48
|
-
<img
|
|
49
|
-
src={slide.imageUrl}
|
|
50
|
-
alt={slide.title}
|
|
51
|
-
className="w-full h-full object-cover group-hover:opacity-30 transition-all duration-300 ease-in-out rounded-2xl"
|
|
52
|
-
/>
|
|
53
|
-
<img
|
|
54
|
-
src={slide.pngUrl}
|
|
55
|
-
alt={slide.title}
|
|
56
|
-
className="absolute inset-0 w-full h-full object-cover group-hover:translate-z-15 transition-all duration-300 ease-in-out rounded-2xl"
|
|
57
|
-
/>
|
|
58
|
-
</div>
|
|
59
|
-
<div
|
|
60
|
-
className="relative flex flex-col justify-end h-full text-white p-8"
|
|
61
|
-
style={{
|
|
62
|
-
transformStyle: 'preserve-3d',
|
|
63
|
-
perspective: '1500px',
|
|
64
|
-
}}
|
|
65
|
-
>
|
|
66
|
-
<h2 className="text-3xl font-bold group-hover:translate-z-18 transition-all duration-300 ease-in-out">{slide.title}</h2>
|
|
67
|
-
<p className="text-lime-400 mt-1 group-hover:translate-z-18 transition-all duration-300 ease-in-out">{slide.subtitle}</p>
|
|
68
|
-
<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>
|
|
69
|
-
</div>
|
|
70
|
-
</motion.div>
|
|
71
|
-
);
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
// --- Custom Parallax Carousel Component ---
|
|
76
|
-
const ParallaxCarousel = ({ slides }) => {
|
|
77
|
-
const [activeIndex, setActiveIndex] = useState(0);
|
|
78
|
-
const [isHovered, setIsHovered] = useState(false);
|
|
79
|
-
|
|
80
|
-
const handleNext = () => {
|
|
81
|
-
setActiveIndex((prevIndex) => (prevIndex + 1) % slides.length);
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
const handlePrev = () => {
|
|
85
|
-
setActiveIndex((prevIndex) => (prevIndex - 1 + slides.length) % slides.length);
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
// Automatic sliding ke liye
|
|
89
|
-
useEffect(() => {
|
|
90
|
-
// Agar hovered hai to interval start hi mat karo
|
|
91
|
-
if (isHovered) {
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const interval = setInterval(() => {
|
|
96
|
-
handleNext();
|
|
97
|
-
}, 4000); // Har 4 second mein slide change hoga
|
|
98
|
-
|
|
99
|
-
return () => clearInterval(interval); // Cleanup
|
|
100
|
-
}, [activeIndex, isHovered]); // isHovered state change hone par effect ko firse chalayega
|
|
101
|
-
|
|
102
|
-
return (
|
|
103
|
-
<div
|
|
104
|
-
className="relative w-full flex flex-col items-center px-2"
|
|
105
|
-
onMouseEnter={() => setIsHovered(true)}
|
|
106
|
-
onMouseLeave={() => setIsHovered(false)}
|
|
107
|
-
>
|
|
108
|
-
{/* 3D Carousel Container */}
|
|
109
|
-
<div
|
|
110
|
-
className="relative w-full h-[480px] portrait:h-[480px] portrait:px-0"
|
|
111
|
-
style={{ perspective: '1200px', transformStyle: 'preserve-3d' }}
|
|
112
|
-
>
|
|
113
|
-
{slides.map((slide, index) => {
|
|
114
|
-
let positionOffset = index - activeIndex;
|
|
115
|
-
if (positionOffset > slides.length / 2) {
|
|
116
|
-
positionOffset -= slides.length;
|
|
117
|
-
} else if (positionOffset < -slides.length / 2) {
|
|
118
|
-
positionOffset += slides.length;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const isVisible = Math.abs(positionOffset) <= 2;
|
|
122
|
-
|
|
123
|
-
return (
|
|
124
|
-
<motion.div
|
|
125
|
-
key={index}
|
|
126
|
-
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`}
|
|
127
|
-
initial={false}
|
|
128
|
-
style={{ transformStyle: 'preserve-3d' }}
|
|
129
|
-
animate={{
|
|
130
|
-
transform: `translateX(${positionOffset * 50}%) translateZ(${-Math.abs(positionOffset) * 250}px) scale(${1 - Math.abs(positionOffset) * 0.15})`,
|
|
131
|
-
opacity: isVisible ? (1 - Math.abs(positionOffset) * 0.45) : 0,
|
|
132
|
-
zIndex: slides.length - Math.abs(positionOffset),
|
|
133
|
-
}}
|
|
134
|
-
transition={{ type: 'spring', stiffness: 290, damping: 30 }}
|
|
135
|
-
>
|
|
136
|
-
<CarouselCard slide={slide} isCenter={positionOffset === 0} />
|
|
137
|
-
</motion.div>
|
|
138
|
-
);
|
|
139
|
-
})}
|
|
140
|
-
</div>
|
|
141
|
-
|
|
142
|
-
{/* Side Navigation Buttons */}
|
|
143
|
-
<motion.button
|
|
144
|
-
onClick={handlePrev}
|
|
145
|
-
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"
|
|
146
|
-
whileTap={{ scale: 0.9 }}
|
|
147
|
-
>
|
|
148
|
-
<ArrowLeft size={32} />
|
|
149
|
-
</motion.button>
|
|
150
|
-
<motion.button
|
|
151
|
-
onClick={handleNext}
|
|
152
|
-
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"
|
|
153
|
-
whileTap={{ scale: 0.9 }}
|
|
154
|
-
>
|
|
155
|
-
<ArrowRight size={32} />
|
|
156
|
-
</motion.button>
|
|
157
|
-
</div>
|
|
158
|
-
);
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
export default ParallaxCarousel;
|
|
@@ -16,10 +16,16 @@ const DragCarousel = ({ images }) => {
|
|
|
16
16
|
const dragProxyRef = useRef(null);
|
|
17
17
|
useEffect(() => {
|
|
18
18
|
const cards = cardsRef.current;
|
|
19
|
+
// hint browser to use the GPU for transforms
|
|
20
|
+
cards.forEach((c) => {
|
|
21
|
+
if (c) c.style.willChange = 'transform, opacity';
|
|
22
|
+
});
|
|
23
|
+
|
|
19
24
|
gsap.to(cards, {
|
|
20
|
-
duration: 0.
|
|
21
|
-
ease: '
|
|
22
|
-
stagger: 0.
|
|
25
|
+
duration: 0.45,
|
|
26
|
+
ease: 'power2.out',
|
|
27
|
+
stagger: 0.03,
|
|
28
|
+
overwrite: true,
|
|
23
29
|
// Har card ke liye naya transform calculate karta hai
|
|
24
30
|
transform: (i) => {
|
|
25
31
|
let positionOffset = i - activeIndex;
|
|
@@ -31,7 +37,7 @@ const DragCarousel = ({ images }) => {
|
|
|
31
37
|
|
|
32
38
|
const translateX = positionOffset * 50;
|
|
33
39
|
const translateZ = -Math.abs(positionOffset) * 250;
|
|
34
|
-
const scale = 1 - Math.abs(positionOffset) * 0.
|
|
40
|
+
const scale = 1 - Math.abs(positionOffset) * 0.12;
|
|
35
41
|
|
|
36
42
|
return `translateX(${translateX}%) translateZ(${translateZ}px) scale(${scale})`;
|
|
37
43
|
},
|
|
@@ -53,14 +59,39 @@ const DragCarousel = ({ images }) => {
|
|
|
53
59
|
// GSAP Draggable ko setup karta hai
|
|
54
60
|
useEffect(() => {
|
|
55
61
|
// BUG FIX: Draggable ab proxy element par lagaya gaya hai
|
|
62
|
+
const cards = cardsRef.current;
|
|
63
|
+
const setters = cards.map((c) => (c ? gsap.quickSetter(c, 'css', 'transform') : null));
|
|
64
|
+
|
|
56
65
|
const draggableInstance = Draggable.create(dragProxyRef.current, {
|
|
57
66
|
type: "x",
|
|
58
67
|
trigger: carouselRef.current,
|
|
59
68
|
inertia: true,
|
|
60
69
|
cursor: 'grab',
|
|
61
70
|
activeCursor: 'grabbing',
|
|
71
|
+
onDrag: function () {
|
|
72
|
+
const w = carouselRef.current ? carouselRef.current.clientWidth : window.innerWidth;
|
|
73
|
+
const shiftPercent = (this.x / w) * 100; // percentage shift based on drag
|
|
74
|
+
|
|
75
|
+
cards.forEach((c, i) => {
|
|
76
|
+
if (!c || !setters[i]) return;
|
|
77
|
+
let positionOffset = i - activeIndex;
|
|
78
|
+
if (positionOffset > images.length / 2) positionOffset -= images.length;
|
|
79
|
+
if (positionOffset < -images.length / 2) positionOffset += images.length;
|
|
80
|
+
|
|
81
|
+
const baseX = positionOffset * 50;
|
|
82
|
+
const translateX = baseX + shiftPercent;
|
|
83
|
+
const translateZ = -Math.abs(positionOffset) * 250;
|
|
84
|
+
const scale = 1 - Math.abs(positionOffset) * 0.12;
|
|
85
|
+
|
|
86
|
+
setters[i](`translateX(${translateX}%) translateZ(${translateZ}px) scale(${scale})`);
|
|
87
|
+
c.style.opacity = Math.abs(positionOffset - (this.x / w)) > 2 ? 0 : 1;
|
|
88
|
+
c.style.zIndex = images.length - Math.abs(positionOffset);
|
|
89
|
+
});
|
|
90
|
+
},
|
|
62
91
|
onDragEnd: function () {
|
|
63
|
-
|
|
92
|
+
// Reset proxy position
|
|
93
|
+
gsap.to(this.target, { x: 0, duration: 0.35, ease: 'power2.out' });
|
|
94
|
+
// Direction based index change
|
|
64
95
|
if (this.getDirection("start") === "left") {
|
|
65
96
|
setActiveIndex(prev => (prev + 1) % images.length);
|
|
66
97
|
} else if (this.getDirection("start") === "right") {
|
|
@@ -74,8 +105,9 @@ const DragCarousel = ({ images }) => {
|
|
|
74
105
|
if (draggableInstance && draggableInstance[0]) {
|
|
75
106
|
draggableInstance[0].kill();
|
|
76
107
|
}
|
|
108
|
+
setters.forEach((s) => s && s.kill && s.kill());
|
|
77
109
|
};
|
|
78
|
-
}, [images.length]);
|
|
110
|
+
}, [images.length, activeIndex]);
|
|
79
111
|
|
|
80
112
|
return (
|
|
81
113
|
<div
|
|
@@ -89,7 +121,7 @@ const DragCarousel = ({ images }) => {
|
|
|
89
121
|
<div
|
|
90
122
|
key={index}
|
|
91
123
|
ref={el => cardsRef.current[index] = el}
|
|
92
|
-
className="card-item absolute h-[480px] w-[300px] lg:h-[550px] lg:w-[350px] rounded-xl overflow-hidden shadow-2xl top-[25px]
|
|
124
|
+
className="card-item absolute h-[480px] w-[min(300px,80vw)] lg:h-[550px] lg:w-[350px] rounded-xl overflow-hidden shadow-2xl top-[25px] left-1/2 -ml-[min(150px,40vw)] lg:-ml-[175px]"
|
|
93
125
|
>
|
|
94
126
|
<img
|
|
95
127
|
src={src}
|