apex-ui-kit 2.0.5
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/Licence.md +21 -0
- package/ReadMe.md +75 -0
- package/dist/ApexUI-Kit/templates/Avatar/Avatar.jsx +81 -0
- package/dist/ApexUI-Kit/templates/BeamBar/BeamBar.jsx +101 -0
- package/dist/ApexUI-Kit/templates/Cards/BasicCard.jsx +60 -0
- package/dist/ApexUI-Kit/templates/Carousel/Carousel.jsx +95 -0
- package/dist/ApexUI-Kit/templates/Carousel/DragCarousel.jsx +105 -0
- package/dist/ApexUI-Kit/templates/Carousel/ParallaxCarousel.jsx +161 -0
- package/dist/ApexUI-Kit/templates/DragCarousel/DragCarousel.jsx +105 -0
- package/dist/ApexUI-Kit/templates/EdgeGlowForm/EdgeGlowFrom.jsx +297 -0
- package/dist/ApexUI-Kit/templates/FlipProfile/FlipProfile.jsx +125 -0
- package/dist/ApexUI-Kit/templates/GlareCard/GlareCard.jsx +146 -0
- package/dist/ApexUI-Kit/templates/GlassCardStack/GlassCardStack.jsx +111 -0
- package/dist/ApexUI-Kit/templates/HoverText/HoverText.jsx +371 -0
- package/dist/ApexUI-Kit/templates/HyperCard/HyperCard.jsx +183 -0
- package/dist/ApexUI-Kit/templates/KineticThreadsBackground/KineticThreadBackground.jsx +249 -0
- package/dist/ApexUI-Kit/templates/LensFlareBackground/LensFlareBackground.jsx +268 -0
- package/dist/ApexUI-Kit/templates/LuminousParticleOcean/LuminousParticleOcean.jsx +155 -0
- package/dist/ApexUI-Kit/templates/NavDropDown/NavDropDown.jsx +114 -0
- package/dist/ApexUI-Kit/templates/NavMenu/NavMenu.jsx +153 -0
- package/dist/ApexUI-Kit/templates/OrbsBackground/FloatingOrbs.jsx +96 -0
- package/dist/ApexUI-Kit/templates/ParallaxCard/ParallaxCard.jsx +127 -0
- package/dist/ApexUI-Kit/templates/ParallaxCarousel/ParallaxCarousel.jsx +161 -0
- package/dist/ApexUI-Kit/templates/ParallaxProfile/ParallaxProfile.jsx +107 -0
- package/dist/ApexUI-Kit/templates/PointerFollower/PointerFollower.jsx +126 -0
- package/dist/ApexUI-Kit/templates/RainBackground/RainBackground.jsx +109 -0
- package/dist/ApexUI-Kit/templates/RippleBackground/RippleBackground.jsx +160 -0
- package/dist/ApexUI-Kit/templates/ScrollMarquee/ScrollMarquee.jsx +174 -0
- package/dist/ApexUI-Kit/templates/SmartGridCard/SmartGridCard.jsx +241 -0
- package/dist/ApexUI-Kit/templates/SmokeBackground/SmokeBackground.jsx +216 -0
- package/dist/ApexUI-Kit/templates/Testimonials/Testimonials.jsx +117 -0
- package/dist/ApexUI-Kit/templates/ThemeToggle/ThemeAnimation.js +252 -0
- package/dist/ApexUI-Kit/templates/ThemeToggle/ThemeConfig.js +8 -0
- package/dist/ApexUI-Kit/templates/ThemeToggle/ThemeToggle.css +560 -0
- package/dist/ApexUI-Kit/templates/ThemeToggle/ThemeToggle.jsx +118 -0
- package/dist/ApexUI-Kit/templates/ToolTip/ToolTip.jsx +85 -0
- package/dist/ApexUI-Kit/templates/WaterDropReveal/WaterDropReveal.jsx +151 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +145 -0
- package/package.json +56 -0
|
@@ -0,0 +1,114 @@
|
|
|
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);
|
|
6
|
+
|
|
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]);
|
|
27
|
+
|
|
28
|
+
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>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const NavDropDown = ({ navData }) => {
|
|
82
|
+
const [openIndex, setOpenIndex] = useState(null);
|
|
83
|
+
|
|
84
|
+
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"
|
|
99
|
+
>
|
|
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>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export default NavDropDown;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect } from 'react';
|
|
2
|
+
import { gsap } from "gsap";
|
|
3
|
+
const NavMenu = ({
|
|
4
|
+
indicatorColor = "#a3e635",
|
|
5
|
+
backgroundColor = "rgba(17, 24, 39, 0.5)",
|
|
6
|
+
activeColor = "#ffffff",
|
|
7
|
+
indicatorAnimation = "elastic",
|
|
8
|
+
onNavItemClick
|
|
9
|
+
}) => {
|
|
10
|
+
const [activeIdx, setActiveIdx] = useState(0);
|
|
11
|
+
const navItemsRef = useRef([]);
|
|
12
|
+
const indicatorRef = useRef(null);
|
|
13
|
+
const navContainerRef = useRef(null);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (!navContainerRef.current) return;
|
|
16
|
+
gsap.fromTo(
|
|
17
|
+
navContainerRef.current,
|
|
18
|
+
{ width: 0, opacity: 0 },
|
|
19
|
+
{ width: "auto", opacity: 1, duration: 0.8, ease: "power2.out", delay: 0.5 }
|
|
20
|
+
);
|
|
21
|
+
}, []);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const indicator = indicatorRef.current;
|
|
25
|
+
const el = navItemsRef.current[activeIdx];
|
|
26
|
+
|
|
27
|
+
if (!indicator || !el) return;
|
|
28
|
+
|
|
29
|
+
const getEase = () => {
|
|
30
|
+
switch (indicatorAnimation) {
|
|
31
|
+
case "spring": return "bounce.out";
|
|
32
|
+
case "power": return "power3.out";
|
|
33
|
+
case "elastic":
|
|
34
|
+
default: return "elastic.out(1, 0.6)";
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const moveWithGSAP = () => {
|
|
39
|
+
const rect = el.getBoundingClientRect();
|
|
40
|
+
const parentRect = el.parentElement.getBoundingClientRect();
|
|
41
|
+
const left = rect.left - parentRect.left;
|
|
42
|
+
const width = rect.width;
|
|
43
|
+
|
|
44
|
+
gsap.to(indicator, {
|
|
45
|
+
x: left,
|
|
46
|
+
width,
|
|
47
|
+
duration: 0.5,
|
|
48
|
+
ease: getEase(),
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const timeoutId = setTimeout(moveWithGSAP, 50);
|
|
53
|
+
return () => clearTimeout(timeoutId);
|
|
54
|
+
|
|
55
|
+
}, [activeIdx, indicatorAnimation]);
|
|
56
|
+
|
|
57
|
+
// Handle resize
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
const handleResize = () => {
|
|
60
|
+
const el = navItemsRef.current[activeIdx];
|
|
61
|
+
if (el) {
|
|
62
|
+
const rect = el.getBoundingClientRect();
|
|
63
|
+
const parentRect = el.parentElement.getBoundingClientRect();
|
|
64
|
+
const left = rect.left - parentRect.left;
|
|
65
|
+
const width = rect.width;
|
|
66
|
+
|
|
67
|
+
gsap.set(indicatorRef.current, {
|
|
68
|
+
x: left,
|
|
69
|
+
width: width
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
window.addEventListener("resize", handleResize);
|
|
75
|
+
return () => window.removeEventListener("resize", handleResize);
|
|
76
|
+
}, [activeIdx]);
|
|
77
|
+
|
|
78
|
+
const navItems = ['Home', 'Docs', 'UI Kit', 'Contact'];
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<nav
|
|
82
|
+
ref={navContainerRef}
|
|
83
|
+
className="rounded px-3 py-2 shadow-lg border border-white/10 overflow-hidden"
|
|
84
|
+
style={{ backdropFilter: 'blur(16px)', WebkitBackdropFilter: 'blur(16px)', background: backgroundColor , }}
|
|
85
|
+
>
|
|
86
|
+
<div
|
|
87
|
+
ref={indicatorRef}
|
|
88
|
+
className="absolute top-1.5 h-[calc(100%-0.75rem)] rounded-lg z-0 "
|
|
89
|
+
style={{ background: indicatorColor, opacity: 0.3 }}
|
|
90
|
+
/>
|
|
91
|
+
<div className="relative flex gap-2 z-10 whitespace-nowrap">
|
|
92
|
+
{navItems.map((label, i) => (
|
|
93
|
+
<button
|
|
94
|
+
key={label}
|
|
95
|
+
ref={(el) => (navItemsRef.current[i] = el)}
|
|
96
|
+
onClick={() => {
|
|
97
|
+
setActiveIdx(i);
|
|
98
|
+
if (onNavItemClick) onNavItemClick();
|
|
99
|
+
}}
|
|
100
|
+
className={`nav-item px-6 py-2 text-base font-semibold rounded-full relative z-10 transition-colors duration-300 ${i === activeIdx ? '' : 'text-gray-400 hover:text-white'}`}
|
|
101
|
+
style={{ color: i === activeIdx ? activeColor : undefined }}
|
|
102
|
+
>
|
|
103
|
+
{label}
|
|
104
|
+
</button>
|
|
105
|
+
))}
|
|
106
|
+
</div>
|
|
107
|
+
</nav>
|
|
108
|
+
);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
// Responsive Wrapper Component
|
|
113
|
+
const ResponsiveNavMenu = (props) => {
|
|
114
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
115
|
+
const navItems = ['Home', 'Docs', 'UI Kit', 'Contact'];
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<nav className="flex items-center justify-center p-4 sticky top-0 left-0 w-full z-50 bg-transparent">
|
|
119
|
+
<div className="hidden md:flex w-full justify-center">
|
|
120
|
+
<NavMenu {...props} />
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<div className="md:hidden flex w-full justify-end">
|
|
124
|
+
<button onClick={() => setIsOpen(!isOpen)} className="text-white focus:outline-none">
|
|
125
|
+
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
126
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={isOpen ? "M6 18L18 6M6 6l12 12" : "M4 6h16M4 12h16m-7 6h7"} />
|
|
127
|
+
</svg>
|
|
128
|
+
</button>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
{isOpen && (
|
|
132
|
+
<div className="md:hidden absolute top-20 left-4 right-4 rounded-xl shadow-lg border border-white/10"
|
|
133
|
+
style={{ backdropFilter: 'blur(16px)', WebkitBackdropFilter: 'blur(16px)', background: 'rgba(17, 24, 39, 0.7)' }}>
|
|
134
|
+
<div className="flex flex-col items-center gap-2 p-4">
|
|
135
|
+
{navItems.map((label, i) => (
|
|
136
|
+
<button
|
|
137
|
+
key={label}
|
|
138
|
+
onClick={() => {
|
|
139
|
+
console.log(`${label} clicked`);
|
|
140
|
+
setIsOpen(false);
|
|
141
|
+
}}
|
|
142
|
+
className="w-full text-center px-6 py-3 text-lg font-semibold rounded-full text-gray-300 hover:text-white hover:bg-white/10 transition-colors duration-300"
|
|
143
|
+
>
|
|
144
|
+
{label}
|
|
145
|
+
</button>
|
|
146
|
+
))}
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
150
|
+
</nav>
|
|
151
|
+
);
|
|
152
|
+
};
|
|
153
|
+
export default ResponsiveNavMenu;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/* eslint-disable no-unused-vars */
|
|
2
|
+
import React, { useState, useEffect } from 'react';
|
|
3
|
+
import { motion } from 'framer-motion';
|
|
4
|
+
|
|
5
|
+
// --- FloatingOrbsBackground Component (Optimized for Mobile) ---
|
|
6
|
+
const FloatingOrbsBackground = ({
|
|
7
|
+
children,
|
|
8
|
+
className,
|
|
9
|
+
containerClassName,
|
|
10
|
+
colors,
|
|
11
|
+
desktopOrbs = 20, // Orbs for desktop
|
|
12
|
+
mobileOrbs = 10, // Orbs for mobile
|
|
13
|
+
}) => {
|
|
14
|
+
const [numberOfOrbs, setNumberOfOrbs] = useState(desktopOrbs);
|
|
15
|
+
const [isMobile, setIsMobile] = useState(false);
|
|
16
|
+
|
|
17
|
+
// Screen size check karne ke liye
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const checkScreenSize = () => {
|
|
20
|
+
const isMobileDevice = window.innerWidth < 768;
|
|
21
|
+
setIsMobile(isMobileDevice);
|
|
22
|
+
setNumberOfOrbs(isMobileDevice ? mobileOrbs : desktopOrbs);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
checkScreenSize(); // Pehli baar check karein
|
|
26
|
+
window.addEventListener('resize', checkScreenSize);
|
|
27
|
+
|
|
28
|
+
// Cleanup
|
|
29
|
+
return () => window.removeEventListener('resize', checkScreenSize);
|
|
30
|
+
}, [desktopOrbs, mobileOrbs]);
|
|
31
|
+
|
|
32
|
+
const orbColors = colors ?? [
|
|
33
|
+
"#10b981", "#06b6d4", "#6366f1", "#a855f7",
|
|
34
|
+
"#d946ef", "#ec4899", "#f43f5e", "#f97316",
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
className={`relative h-[60vh] lg:h-[80vh] w-full flex flex-col items-center justify-center overflow-hidden bg-slate-950 ${containerClassName}`}
|
|
40
|
+
>
|
|
41
|
+
{/* Orbs Container */}
|
|
42
|
+
<div className="absolute inset-0 z-0 w-full h-full">
|
|
43
|
+
{Array.from({ length: numberOfOrbs }).map((_, index) => {
|
|
44
|
+
const size = Math.random() * 200 + 100;
|
|
45
|
+
const duration = Math.random() * 20 + 15;
|
|
46
|
+
// Mobile ke liye blur kam kar diya hai
|
|
47
|
+
const blur = isMobile ? '60px' : '80px';
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<motion.div
|
|
51
|
+
key={index}
|
|
52
|
+
className="absolute rounded-full"
|
|
53
|
+
style={{
|
|
54
|
+
width: size,
|
|
55
|
+
height: size,
|
|
56
|
+
backgroundColor: orbColors[index % orbColors.length],
|
|
57
|
+
filter: `blur(${blur})`,
|
|
58
|
+
opacity: 0.4,
|
|
59
|
+
// Performance behtar karne ke liye browser ko hint dena
|
|
60
|
+
willChange: 'transform, opacity',
|
|
61
|
+
}}
|
|
62
|
+
animate={{
|
|
63
|
+
x: [
|
|
64
|
+
// Movement range ko badha diya hai taaki yeh poori screen cover kare
|
|
65
|
+
`${Math.random() * 150 - 75}vw`,
|
|
66
|
+
`${Math.random() * 150 - 75}vw`,
|
|
67
|
+
`${Math.random() * 150 - 75}vw`,
|
|
68
|
+
],
|
|
69
|
+
y: [
|
|
70
|
+
// Movement range ko badha diya hai taaki yeh poori screen cover kare
|
|
71
|
+
`${Math.random() * 150 - 75}vh`,
|
|
72
|
+
`${Math.random() * 150 - 75}vh`,
|
|
73
|
+
`${Math.random() * 150 - 75}vh`,
|
|
74
|
+
],
|
|
75
|
+
scale: [1, 1.2, 1],
|
|
76
|
+
}}
|
|
77
|
+
transition={{
|
|
78
|
+
duration: duration,
|
|
79
|
+
repeat: Infinity,
|
|
80
|
+
ease: 'easeInOut',
|
|
81
|
+
repeatType: 'mirror',
|
|
82
|
+
}}
|
|
83
|
+
/>
|
|
84
|
+
);
|
|
85
|
+
})}
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div className={`relative z-10 ${className}`}>
|
|
89
|
+
{children}
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
export default FloatingOrbsBackground;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import React, { useRef, useEffect } from 'react';
|
|
2
|
+
import { gsap } from 'gsap';
|
|
3
|
+
import { ArrowRight } from 'lucide-react';
|
|
4
|
+
import { User } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
// --- Multi-Layered 3D Parallax Card Component ---
|
|
7
|
+
const ParallaxCard = ({ cardData = [], tiltEnable, glareEnable, scale, perspective }) => {
|
|
8
|
+
// Helper for per-card refs
|
|
9
|
+
const cardRefs = useRef([]);
|
|
10
|
+
const titleRefs = useRef([]);
|
|
11
|
+
const imageRefs = useRef([]);
|
|
12
|
+
const descRefs = useRef([]);
|
|
13
|
+
const buttonRefs = useRef([]);
|
|
14
|
+
const linkRefs = useRef([]);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
cardData.forEach((_, idx) => {
|
|
18
|
+
const cardElement = cardRefs.current[idx];
|
|
19
|
+
const titleElement = titleRefs.current[idx];
|
|
20
|
+
const imageElement = imageRefs.current[idx];
|
|
21
|
+
const descElement = descRefs.current[idx];
|
|
22
|
+
const buttonElement = buttonRefs.current[idx];
|
|
23
|
+
const linkElement = linkRefs.current[idx];
|
|
24
|
+
|
|
25
|
+
if (!cardElement) return;
|
|
26
|
+
|
|
27
|
+
const handleMouseMove = (e) => {
|
|
28
|
+
if (!tiltEnable) return;
|
|
29
|
+
const { left, top, width, height } = cardElement.getBoundingClientRect();
|
|
30
|
+
const x = e.clientX - left;
|
|
31
|
+
const y = e.clientY - top;
|
|
32
|
+
const rotateX = (y / height - 0.5) * -30;
|
|
33
|
+
const rotateY = (x / width - 0.5) * 30;
|
|
34
|
+
|
|
35
|
+
gsap.to(cardElement, {
|
|
36
|
+
duration: 1,
|
|
37
|
+
rotationX: rotateX,
|
|
38
|
+
rotationY: rotateY,
|
|
39
|
+
scale: scale,
|
|
40
|
+
ease: "power3.out",
|
|
41
|
+
});
|
|
42
|
+
|
|
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 });
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const handleMouseLeave = () => {
|
|
51
|
+
if (tiltEnable) {
|
|
52
|
+
gsap.to(cardElement, {
|
|
53
|
+
duration: 1.2,
|
|
54
|
+
rotationX: 0,
|
|
55
|
+
rotationY: 0,
|
|
56
|
+
scale: 1,
|
|
57
|
+
ease: "elastic.out(1, 0.5)",
|
|
58
|
+
});
|
|
59
|
+
gsap.to([titleElement, imageElement, descElement, buttonElement, linkElement], {
|
|
60
|
+
duration: 1.2,
|
|
61
|
+
x: 0,
|
|
62
|
+
y: 0,
|
|
63
|
+
ease: "elastic.out(1, 0.5)",
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
cardElement.addEventListener("mousemove", handleMouseMove);
|
|
69
|
+
cardElement.addEventListener("mouseleave", handleMouseLeave);
|
|
70
|
+
|
|
71
|
+
return () => {
|
|
72
|
+
cardElement.removeEventListener("mousemove", handleMouseMove);
|
|
73
|
+
cardElement.removeEventListener("mouseleave", handleMouseLeave);
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
}, [cardData, tiltEnable, scale]);
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div className="flex flex-wrap gap-8 justify-center">
|
|
80
|
+
{cardData.map((card, idx) => (
|
|
81
|
+
<div
|
|
82
|
+
key={idx}
|
|
83
|
+
ref={el => cardRefs.current[idx] = el}
|
|
84
|
+
className="w-full max-w-md rounded-2xl"
|
|
85
|
+
style={{ transformStyle: 'preserve-3d', perspective: `${perspective}px` }}
|
|
86
|
+
>
|
|
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"
|
|
88
|
+
style={{ transformStyle: 'preserve-3d' }}
|
|
89
|
+
>
|
|
90
|
+
{glareEnable && (
|
|
91
|
+
<div className="absolute inset-0 w-full h-full bg-gradient-to-br from-white/10 to-transparent opacity-50 pointer-events-none z-0" style={{ transform: 'translateZ(100px)' }}></div>
|
|
92
|
+
)}
|
|
93
|
+
|
|
94
|
+
<div className="relative z-10 flex flex-col h-full">
|
|
95
|
+
<div ref={el => titleRefs.current[idx] = el} className="text-start" style={{ transform: 'translateZ(40px)' }}>
|
|
96
|
+
<div className='flex items-center gap-3'>
|
|
97
|
+
<User /> <h2 className="md:text-3xl font-bold text-white">{card.title}</h2>
|
|
98
|
+
|
|
99
|
+
</div>
|
|
100
|
+
<p className="text-lime-400 mt-1">{card.subtitle}</p>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div ref={el => imageRefs.current[idx] = el} className="my-6" style={{ transform: 'translateZ(80px)' }}>
|
|
104
|
+
<img
|
|
105
|
+
src={card.imageUrl}
|
|
106
|
+
alt={card.title}
|
|
107
|
+
className="w-full md:h-56 object-cover rounded-lg shadow-lg shadow-black/40"
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<div ref={el => descRefs.current[idx] = el} className="text-start" style={{ transform: 'translateZ(30px)' }}>
|
|
112
|
+
<p className="text-slate-400 md:text-sm text-xs mb-6">{card.description}</p>
|
|
113
|
+
</div>
|
|
114
|
+
<div ref={el => buttonRefs.current[idx] = el} className="text-center flex items-center justify-end" style={{ transform: 'translateZ(30px)' }}>
|
|
115
|
+
<a href={card.link} className="border border-lime-400 py-2 px-3 rounded-full flex items-center gap-2 font-medium text-white group hover:text-lime-400 transition-colors duration-300">
|
|
116
|
+
Learn More <ArrowRight size={16} className="transition-transform duration-300 group-hover:translate-x-1 translate-y-0.5" />
|
|
117
|
+
</a>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
))}
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export default ParallaxCard;
|
|
@@ -0,0 +1,161 @@
|
|
|
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;
|