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.
Files changed (40) hide show
  1. package/Licence.md +21 -0
  2. package/ReadMe.md +75 -0
  3. package/dist/ApexUI-Kit/templates/Avatar/Avatar.jsx +81 -0
  4. package/dist/ApexUI-Kit/templates/BeamBar/BeamBar.jsx +101 -0
  5. package/dist/ApexUI-Kit/templates/Cards/BasicCard.jsx +60 -0
  6. package/dist/ApexUI-Kit/templates/Carousel/Carousel.jsx +95 -0
  7. package/dist/ApexUI-Kit/templates/Carousel/DragCarousel.jsx +105 -0
  8. package/dist/ApexUI-Kit/templates/Carousel/ParallaxCarousel.jsx +161 -0
  9. package/dist/ApexUI-Kit/templates/DragCarousel/DragCarousel.jsx +105 -0
  10. package/dist/ApexUI-Kit/templates/EdgeGlowForm/EdgeGlowFrom.jsx +297 -0
  11. package/dist/ApexUI-Kit/templates/FlipProfile/FlipProfile.jsx +125 -0
  12. package/dist/ApexUI-Kit/templates/GlareCard/GlareCard.jsx +146 -0
  13. package/dist/ApexUI-Kit/templates/GlassCardStack/GlassCardStack.jsx +111 -0
  14. package/dist/ApexUI-Kit/templates/HoverText/HoverText.jsx +371 -0
  15. package/dist/ApexUI-Kit/templates/HyperCard/HyperCard.jsx +183 -0
  16. package/dist/ApexUI-Kit/templates/KineticThreadsBackground/KineticThreadBackground.jsx +249 -0
  17. package/dist/ApexUI-Kit/templates/LensFlareBackground/LensFlareBackground.jsx +268 -0
  18. package/dist/ApexUI-Kit/templates/LuminousParticleOcean/LuminousParticleOcean.jsx +155 -0
  19. package/dist/ApexUI-Kit/templates/NavDropDown/NavDropDown.jsx +114 -0
  20. package/dist/ApexUI-Kit/templates/NavMenu/NavMenu.jsx +153 -0
  21. package/dist/ApexUI-Kit/templates/OrbsBackground/FloatingOrbs.jsx +96 -0
  22. package/dist/ApexUI-Kit/templates/ParallaxCard/ParallaxCard.jsx +127 -0
  23. package/dist/ApexUI-Kit/templates/ParallaxCarousel/ParallaxCarousel.jsx +161 -0
  24. package/dist/ApexUI-Kit/templates/ParallaxProfile/ParallaxProfile.jsx +107 -0
  25. package/dist/ApexUI-Kit/templates/PointerFollower/PointerFollower.jsx +126 -0
  26. package/dist/ApexUI-Kit/templates/RainBackground/RainBackground.jsx +109 -0
  27. package/dist/ApexUI-Kit/templates/RippleBackground/RippleBackground.jsx +160 -0
  28. package/dist/ApexUI-Kit/templates/ScrollMarquee/ScrollMarquee.jsx +174 -0
  29. package/dist/ApexUI-Kit/templates/SmartGridCard/SmartGridCard.jsx +241 -0
  30. package/dist/ApexUI-Kit/templates/SmokeBackground/SmokeBackground.jsx +216 -0
  31. package/dist/ApexUI-Kit/templates/Testimonials/Testimonials.jsx +117 -0
  32. package/dist/ApexUI-Kit/templates/ThemeToggle/ThemeAnimation.js +252 -0
  33. package/dist/ApexUI-Kit/templates/ThemeToggle/ThemeConfig.js +8 -0
  34. package/dist/ApexUI-Kit/templates/ThemeToggle/ThemeToggle.css +560 -0
  35. package/dist/ApexUI-Kit/templates/ThemeToggle/ThemeToggle.jsx +118 -0
  36. package/dist/ApexUI-Kit/templates/ToolTip/ToolTip.jsx +85 -0
  37. package/dist/ApexUI-Kit/templates/WaterDropReveal/WaterDropReveal.jsx +151 -0
  38. package/dist/cli/index.d.ts +2 -0
  39. package/dist/cli/index.js +145 -0
  40. package/package.json +56 -0
@@ -0,0 +1,241 @@
1
+ import React, { useEffect, useRef, useState } from "react";
2
+ import { gsap } from "gsap";
3
+ import {
4
+ Box,
5
+ Settings,
6
+ Lock,
7
+ Sparkles,
8
+ Search,
9
+ LaptopMinimal,
10
+ } from "lucide-react";
11
+
12
+ const CARD_HEIGHT = 250;
13
+ const CARD_EXPANDED_HEIGHT = 510;
14
+
15
+ const SmartGridCard = ({
16
+ cards = defaultCards(),
17
+ mergeMap = {}, // Example: { 5: 2 }
18
+ borderColor = "cyan"
19
+ }) => {
20
+ // Track screen width for responsive mergeMap logic
21
+ const [screenWidth, setScreenWidth] = useState(() => typeof window !== 'undefined' ? window.innerWidth : 1200);
22
+ useEffect(() => {
23
+ const handleResize = () => setScreenWidth(window.innerWidth);
24
+ window.addEventListener('resize', handleResize);
25
+ return () => window.removeEventListener('resize', handleResize);
26
+ }, []);
27
+ const isLargeScreen = screenWidth >= 1080;
28
+ const isThreeCol = screenWidth >= 1024;
29
+ const isTwoCol = screenWidth >= 768 && screenWidth < 1024;
30
+ const cardRefs = useRef({});
31
+ const [mousePos, setMousePos] = useState({});
32
+
33
+ useEffect(() => {
34
+ if (!isLargeScreen) return; // Only run mergeMap effect on large screens
35
+ // Defensive: never trigger a reload, only animate
36
+ if (Object.keys(mergeMap).length === 0) return; // No merges to process
37
+ // Animate cards based on mergeMap
38
+ Object.entries(mergeMap).forEach(([sourceId, targetId]) => {
39
+ const sourceEl = cardRefs.current[sourceId];
40
+ const targetEl = cardRefs.current[targetId];
41
+
42
+ if (targetEl) {
43
+ gsap.to(targetEl, {
44
+ height: CARD_EXPANDED_HEIGHT,
45
+ duration: 0.5,
46
+ ease: "power2.out",
47
+ });
48
+ }
49
+
50
+ if (sourceEl) {
51
+ gsap.to(sourceEl, {
52
+ opacity: 0,
53
+ scale: 0.9,
54
+ duration: 0.3,
55
+ onComplete: () => {
56
+ if (sourceEl) sourceEl.style.display = "none";
57
+ },
58
+ });
59
+ }
60
+ });
61
+ // No reload, no navigation, no window.location
62
+ return () => { };
63
+ }, [mergeMap, isLargeScreen]);
64
+
65
+ const handleMouseMove = (e, id) => {
66
+ const bounds = e.currentTarget.getBoundingClientRect();
67
+ const x = e.clientX - bounds.left;
68
+ const y = e.clientY - bounds.top;
69
+ setMousePos((prev) => ({
70
+ ...prev,
71
+ [id]: { x, y },
72
+ }));
73
+ };
74
+
75
+ const getCardStyle = (id) => {
76
+ // <768px: 1 column
77
+ if (screenWidth < 768) {
78
+ return {
79
+ display: 'block',
80
+ height: CARD_HEIGHT,
81
+ gridColumnStart: 'auto',
82
+ gridRowStart: 'auto',
83
+ };
84
+ }
85
+ // 768px - 1023px: 2 columns
86
+ if (isTwoCol) {
87
+ const col = ((id - 1) % 2) + 1;
88
+ const row = Math.floor((id - 1) / 2) + 1;
89
+ return {
90
+ display: 'block',
91
+ height: CARD_HEIGHT,
92
+ gridColumnStart: col,
93
+ gridRowStart: row,
94
+ };
95
+ }
96
+ // 1024px and above: 3 columns (mergeMap only for >=1080px)
97
+ if (isThreeCol) {
98
+ if (screenWidth < 1080) {
99
+ const col = ((id - 1) % 3) + 1;
100
+ const row = Math.floor((id - 1) / 3) + 1;
101
+ return {
102
+ display: 'block',
103
+ height: CARD_HEIGHT,
104
+ gridColumnStart: col,
105
+ gridRowStart: row,
106
+ };
107
+ }
108
+ // >=1080px: 3 columns, merging enabled
109
+ const isMerged = mergeMap[id];
110
+ const isTarget = Object.values(mergeMap).includes(id);
111
+ if (isMerged) return { display: "none" };
112
+ const col = ((id - 1) % 3) + 1;
113
+ const row = Math.floor((id - 1) / 3) + 1;
114
+ return {
115
+ gridColumnStart: col,
116
+ gridRowStart: row,
117
+ height: isTarget ? CARD_EXPANDED_HEIGHT : CARD_HEIGHT,
118
+ };
119
+ }
120
+ };
121
+
122
+ return (
123
+ <div
124
+ className={`grid gap-2 justify-center p-4 w-full ${screenWidth >= 1024 ? 'grid-cols-3' : screenWidth >= 768 ? 'grid-cols-2' : 'grid-cols-1'}`}
125
+ style={screenWidth >= 1024 ? {
126
+ gridTemplateColumns: "repeat(3, minmax(300px, 1fr))",
127
+ gridTemplateRows: `repeat(2, ${CARD_HEIGHT}px)`
128
+ } : screenWidth >= 768 ? {
129
+ gridTemplateColumns: "repeat(2, minmax(300px, 1fr))",
130
+ gridTemplateRows: `repeat(3, ${CARD_HEIGHT}px)`
131
+ } : {}}
132
+ >
133
+ {cards.map((card) => {
134
+ const style = getCardStyle(card.id);
135
+ if (style.display === "none") return null;
136
+
137
+ const pos = mousePos[card.id] || { x: 0, y: 0 };
138
+
139
+ return (
140
+ <div
141
+ key={card.id}
142
+ ref={(el) => (cardRefs.current[card.id] = el)}
143
+ style={style}
144
+ onMouseMove={(e) => handleMouseMove(e, card.id)}
145
+ className="group relative overflow-hidden p-[1px] rounded-2xl bg-zinc-900 border border-zinc-700 shadow-2xl transition-all duration-500"
146
+ >
147
+ {/* Mouse Glow Border */}
148
+ <div
149
+ className="pointer-events-none absolute inset-0 rounded-2xl opacity-0 group-hover:opacity-100 transition duration-300"
150
+ style={{
151
+ maskImage: `radial-gradient(circle at ${pos.x}px ${pos.y}px, white 40%, transparent 80%)`,
152
+ WebkitMaskImage: `radial-gradient(circle at ${pos.x}px ${pos.y}px, white 40%, transparent 80%)`,
153
+ background: `radial-gradient(circle at ${pos.x}px ${pos.y}px, ${borderColor}, transparent 80%)`,
154
+ borderRadius: "1rem",
155
+ }}
156
+ />
157
+
158
+ {/* Card Content */}
159
+ <div className="relative z-10 flex flex-col items-start justify-between h-full w-full p-6 rounded-2xl bg-zinc-900 text-white space-y-4">
160
+ <div className="text-4xl border border-dashed p-2 rounded-lg border-purple-600">{card.icon}</div>
161
+ <div>
162
+ <h3 className="text-xl md:text-xl font-bold mb-2 break-words leading-tight">{card.title}</h3>
163
+ <p className="text-sm md:text-base w-full text-white/70 break-words">{card.description}</p>
164
+ </div>
165
+ <button
166
+ type="button"
167
+ className="mt-auto relative overflow-hidden px-5 py-3 rounded-lg border border-purple-400/60 text-white transition hover:text-white bg-black/20 group flex items-center gap-2 text-xs md:text-base"
168
+ >
169
+ <span className="relative z-10 flex items-center gap-2">
170
+ {card.buttonText}
171
+ <span className="arrow-icon transition-transform duration-200 group-hover:translate-x-2 flex items-center">
172
+ <svg width="20" height="20" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
173
+ <path d="M5 12h14M13 6l6 6-6 6" />
174
+ </svg>
175
+ </span>
176
+ </span>
177
+ <span
178
+ className="absolute bottom-0 left-0 w-full h-full opacity-0 group-hover:opacity-100 origin-bottom transition-all ease-in-out duration-500 rounded-lg"
179
+ style={{
180
+ background: "radial-gradient(circle at bottom, purple, transparent 69%)",
181
+ mixBlendMode: "screen",
182
+ }}
183
+ />
184
+ </button>
185
+ </div>
186
+ </div>
187
+ );
188
+ })}
189
+ </div>
190
+ );
191
+ };
192
+
193
+ // Default Cards
194
+ function defaultCards() {
195
+ return [
196
+ {
197
+ id: 1,
198
+ icon: <LaptopMinimal className="w-6 h-6 text-white" />,
199
+ title: "Responsive Layout",
200
+ description: "Works across all screen sizes with smooth animation.",
201
+ buttonText: "Learn More",
202
+ },
203
+ {
204
+ id: 2,
205
+ icon: <Box className="w-6 h-6 text-white" />,
206
+ title: "Composable Components",
207
+ description: "Each card is modular and easy to customize.",
208
+ buttonText: "Explore",
209
+ },
210
+ {
211
+ id: 3,
212
+ icon: <Settings className="w-6 h-6 text-white" />,
213
+ title: "Easy Config",
214
+ description: "Use props like mergeMap to control logic.",
215
+ buttonText: "Settings",
216
+ },
217
+ {
218
+ id: 4,
219
+ icon: <Lock className="w-6 h-6 text-white" />,
220
+ title: "Secure UI",
221
+ description: "Perfect for dashboards and secure apps.",
222
+ buttonText: "Secure Now",
223
+ },
224
+ {
225
+ id: 5,
226
+ icon: <Sparkles className="w-6 h-6 text-white" />,
227
+ title: "Edge Effects",
228
+ description: "Glowing borders that follow your mouse.",
229
+ buttonText: "Try It",
230
+ },
231
+ {
232
+ id: 6,
233
+ icon: <Search className="w-6 h-6 text-white" />,
234
+ title: "Fast Search",
235
+ description: "Lightning-fast component navigation.",
236
+ buttonText: "Search",
237
+ },
238
+ ];
239
+ }
240
+
241
+ export default SmartGridCard;
@@ -0,0 +1,216 @@
1
+ import React, { useEffect, useRef } from "react";
2
+ import { Renderer, Program, Mesh, Triangle } from "https://unpkg.com/ogl";
3
+
4
+ const hexToRgb = (hex) => {
5
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
6
+ if (!result) return [1, 0.5, 0.2];
7
+ return [
8
+ parseInt(result[1], 16) / 255,
9
+ parseInt(result[2], 16) / 255,
10
+ parseInt(result[3], 16) / 255,
11
+ ];
12
+ };
13
+
14
+ const vertex = `#version 300 es
15
+ precision highp float;
16
+ in vec2 position;
17
+ in vec2 uv;
18
+ out vec2 vUv;
19
+ void main() {
20
+ vUv = uv;
21
+ gl_Position = vec4(position, 0.0, 1.0);
22
+ }
23
+ `;
24
+
25
+ const fragment = `#version 300 es
26
+ precision highp float;
27
+ uniform vec2 iResolution;
28
+ uniform float iTime;
29
+ uniform vec3 uCustomColor;
30
+ uniform float uUseCustomColor;
31
+ uniform float uSpeed;
32
+ uniform float uDirection;
33
+ uniform float uScale;
34
+ uniform float uOpacity;
35
+ uniform vec2 uMouse;
36
+ uniform float uMouseInteractive;
37
+ out vec4 fragColor;
38
+
39
+ void mainImage(out vec4 o, vec2 C) {
40
+ vec2 center = iResolution.xy * 0.5;
41
+ C = (C - center) / uScale + center;
42
+
43
+ vec2 mouseOffset = (uMouse - center) * 0.0002;
44
+ C += mouseOffset * length(C - center) * step(0.5, uMouseInteractive);
45
+
46
+ float i, d, z, T = iTime * uSpeed * uDirection;
47
+ vec3 O, p, S;
48
+
49
+ // loop count reduced to 30
50
+ for (vec2 r = iResolution.xy, Q; ++i < 35.; O += o.w/d*o.xyz) {
51
+ p = z*normalize(vec3(C-.5*r,r.y));
52
+ p.z -= 4.;
53
+ S = p;
54
+ d = p.y-T;
55
+
56
+ p.x += .4*(1.+p.y)*sin(d + p.x*0.1)*cos(.34*d + p.x*0.05);
57
+ Q = p.xz *= mat2(cos(p.y+vec4(0,11,33,0)-T));
58
+ z+= d = abs(sqrt(length(Q*Q)) - .25*(5.+S.y))/3.+8e-4;
59
+ o = 1.+sin(S.y+p.z*.5+S.z-length(S-p)+vec4(2,1,0,8));
60
+ }
61
+
62
+ o.xyz = tanh(O/1e4);
63
+ }
64
+
65
+ bool finite1(float x){ return !(isnan(x) || isinf(x)); }
66
+ vec3 sanitize(vec3 c){
67
+ return vec3(
68
+ finite1(c.r) ? c.r : 0.0,
69
+ finite1(c.g) ? c.g : 0.0,
70
+ finite1(c.b) ? c.b : 0.0
71
+ );
72
+ }
73
+
74
+ void main() {
75
+ vec4 o = vec4(0.0);
76
+ mainImage(o, gl_FragCoord.xy);
77
+ vec3 rgb = sanitize(o.rgb);
78
+
79
+ float intensity = (rgb.r + rgb.g + rgb.b) / 3.0;
80
+ vec3 customColor = intensity * uCustomColor;
81
+ vec3 finalColor = mix(rgb, customColor, step(0.5, uUseCustomColor));
82
+
83
+ float alpha = length(rgb) * uOpacity;
84
+ fragColor = vec4(finalColor, alpha);
85
+ }
86
+ `;
87
+
88
+
89
+ export const SmokeBackground = ({
90
+ color = "#ffffff",
91
+ speed = 1,
92
+ direction = "forward",
93
+ scale = 1,
94
+ opacity = 1,
95
+ mouseInteractive = true,
96
+ className = "",
97
+ }) => {
98
+ const containerRef = useRef(null);
99
+ const mousePos = useRef({ x: 0, y: 0 });
100
+
101
+ useEffect(() => {
102
+ if (!containerRef.current) return;
103
+
104
+ const useCustomColor = color ? 1.0 : 0.0;
105
+ const customColorRgb = color ? hexToRgb(color) : [1, 1, 1];
106
+
107
+ let directionMultiplier = direction === "backward" ? -1.0 : 1.0;
108
+
109
+ const renderer = new Renderer({
110
+ webgl: 2,
111
+ alpha: true,
112
+ antialias: false,
113
+ dpr: Math.min(window.devicePixelRatio || 1, 1.8),
114
+ });
115
+
116
+ const gl = renderer.gl;
117
+ const canvas = gl.canvas;
118
+ canvas.style.display = "block";
119
+ canvas.style.width = "100%";
120
+ canvas.style.height = "100%";
121
+ containerRef.current.appendChild(canvas);
122
+
123
+ const geometry = new Triangle(gl);
124
+
125
+ const program = new Program(gl, {
126
+ vertex: vertex,
127
+ fragment: fragment,
128
+ uniforms: {
129
+ iTime: { value: 0 },
130
+ iResolution: { value: new Float32Array([1, 1]) },
131
+ uCustomColor: { value: new Float32Array(customColorRgb) },
132
+ uUseCustomColor: { value: useCustomColor },
133
+ uSpeed: { value: speed * 0.4 },
134
+ uDirection: { value: directionMultiplier },
135
+ uScale: { value: scale },
136
+ uOpacity: { value: opacity },
137
+ uMouse: { value: new Float32Array([0, 0]) },
138
+ uMouseInteractive: { value: mouseInteractive ? 1.0 : 0.0 },
139
+ },
140
+ });
141
+
142
+ const mesh = new Mesh(gl, { geometry, program });
143
+
144
+ // 🖱 Mouse throttled to RAF
145
+ let mouseNeedsUpdate = false;
146
+ const handleMouseMove = (e) => {
147
+ if (!mouseInteractive) return;
148
+ const rect = containerRef.current.getBoundingClientRect();
149
+ mousePos.current.x = e.clientX - rect.left;
150
+ mousePos.current.y = rect.height - (e.clientY - rect.top);
151
+ mouseNeedsUpdate = true;
152
+ };
153
+ if (mouseInteractive) {
154
+ containerRef.current.addEventListener("mousemove", handleMouseMove);
155
+ }
156
+
157
+ const setSize = () => {
158
+ const rect = containerRef.current.getBoundingClientRect();
159
+ const width = Math.max(1, Math.floor(rect.width));
160
+ const height = Math.max(1, Math.floor(rect.height));
161
+ renderer.setSize(width, height);
162
+ const res = program.uniforms.iResolution.value;
163
+ res[0] = gl.drawingBufferWidth;
164
+ res[1] = gl.drawingBufferHeight;
165
+ };
166
+
167
+ const ro = new ResizeObserver(() => {
168
+ requestAnimationFrame(setSize);
169
+ });
170
+ ro.observe(containerRef.current);
171
+ setSize();
172
+
173
+ let raf = 0;
174
+ const t0 = performance.now();
175
+ const loop = (t) => {
176
+ let timeValue = (t - t0) * 0.001;
177
+
178
+ if (direction === "funny") {
179
+ const cycle = Math.sin(timeValue * 0.5);
180
+ program.uniforms.uDirection.value = cycle;
181
+ }
182
+
183
+ if (mouseNeedsUpdate) {
184
+ const mouseUniform = program.uniforms.uMouse.value;
185
+ mouseUniform[0] = mousePos.current.x;
186
+ mouseUniform[1] = mousePos.current.y;
187
+ mouseNeedsUpdate = false;
188
+ }
189
+
190
+ program.uniforms.iTime.value = timeValue;
191
+ renderer.render({ scene: mesh });
192
+ raf = requestAnimationFrame(loop);
193
+ };
194
+ raf = requestAnimationFrame(loop);
195
+
196
+ return () => {
197
+ cancelAnimationFrame(raf);
198
+ ro.disconnect();
199
+ if (mouseInteractive && containerRef.current) {
200
+ containerRef.current.removeEventListener("mousemove", handleMouseMove);
201
+ }
202
+ try {
203
+ containerRef.current?.removeChild(canvas);
204
+ } catch (error) { console.error("Error removing canvas:", error); }
205
+ };
206
+ }, [color, speed, direction, scale, opacity, mouseInteractive, className]);
207
+
208
+ return (
209
+ <div
210
+ ref={containerRef}
211
+ className={`w-screen lg:h-full h-[80vh] overflow-hidden relative ${className}`}
212
+ />
213
+ );
214
+ };
215
+
216
+ export default SmokeBackground;
@@ -0,0 +1,117 @@
1
+ /* eslint-disable no-unused-vars */
2
+ import React, { useState } from 'react';
3
+ import { motion, AnimatePresence } from "framer-motion";
4
+ import { ArrowLeft, ArrowRight } from 'lucide-react';
5
+
6
+
7
+ // --- Testimonials Component ---
8
+ const Testimonials = ({ testimonials }) => {
9
+ const [activeIndex, setActiveIndex] = useState(0);
10
+
11
+ const handleNext = () => {
12
+ setActiveIndex((prevIndex) => (prevIndex + 1) % testimonials.length);
13
+ };
14
+
15
+ const handlePrev = () => {
16
+ setActiveIndex((prevIndex) => (prevIndex - 1 + testimonials.length) % testimonials.length);
17
+ };
18
+
19
+ // Animation variants for the text content
20
+ const textVariants = {
21
+ enter: { opacity: 0, y: 20, filter: "blur(5px)" },
22
+ center: { opacity: 1, y: 0, filter: "blur(0px)" },
23
+ exit: { opacity: 0, y: -20, filter: "blur(5px)" },
24
+ };
25
+
26
+ return (
27
+ <div className="w-full max-w-4xl mx-auto bg-slate-950 rounded-2xl p-8 md:p-12 border border-slate-800 grid md:grid-cols-2 gap-12 items-center overflow-hidden">
28
+
29
+ {/* Left Part: Text Content and Navigation */}
30
+ <div className="flex flex-col justify-center h-full text-center md:text-left order-2 md:order-1">
31
+ {/* Title and Subtitle with Animation */}
32
+ <div className="relative h-32">
33
+ <AnimatePresence initial={false} mode="wait">
34
+ <motion.div
35
+ key={activeIndex}
36
+ variants={textVariants}
37
+ initial="enter"
38
+ animate="center"
39
+ exit="exit"
40
+ transition={{ duration: 0.4, ease: "easeInOut" }}
41
+ className="absolute w-full"
42
+ >
43
+ <h2 className="text-3xl font-bold text-white tracking-tight">{testimonials[activeIndex].author}</h2>
44
+ <p className="mt-2 text-lime-400 font-medium">{testimonials[activeIndex].title}</p>
45
+ <p className="mt-4 text-slate-400 text-sm leading-relaxed hidden md:block">"{testimonials[activeIndex].text}"</p>
46
+ </motion.div>
47
+ </AnimatePresence>
48
+ </div>
49
+
50
+ {/* Navigation Buttons with Microinteractions */}
51
+ <div className="flex items-center justify-center md:justify-start gap-4 mt-8">
52
+ <motion.button
53
+ onClick={handlePrev}
54
+ className="bg-slate-800/80 hover:bg-slate-800 rounded-full p-3 transition-colors duration-300 text-slate-400 hover:text-lime-400 focus:outline-none"
55
+ whileHover={{ scale: 1.1 }}
56
+ whileTap={{ scale: 0.95 }}
57
+ transition={{ type: 'spring', stiffness: 400, damping: 17 }}
58
+ >
59
+ <ArrowLeft size={20} />
60
+ </motion.button>
61
+ <motion.button
62
+ onClick={handleNext}
63
+ className="bg-slate-800/80 hover:bg-slate-800 rounded-full p-3 transition-colors duration-300 text-slate-400 hover:text-lime-400 focus:outline-none"
64
+ whileHover={{ scale: 1.1 }}
65
+ whileTap={{ scale: 0.95 }}
66
+ transition={{ type: 'spring', stiffness: 400, damping: 17 }}
67
+ >
68
+ <ArrowRight size={20} />
69
+ </motion.button>
70
+ </div>
71
+ </div>
72
+
73
+ {/* Right Part: Stacked Images */}
74
+ <div className="relative h-80 w-full flex items-center justify-center order-1 md:order-2">
75
+ {testimonials.map((testimonial, index) => {
76
+ // Determine the position of the card in the stack
77
+ const position = (index - activeIndex + testimonials.length) % testimonials.length;
78
+
79
+ // Only render the top 4 cards for performance and visual clarity
80
+ if (position > 3) return null;
81
+
82
+ return (
83
+ <motion.div
84
+ key={testimonial.id || index}
85
+ className="absolute w-64 h-80 bg-slate-800 rounded-xl shadow-2xl overflow-hidden border-2 border-slate-700"
86
+ initial={false}
87
+ animate={{
88
+ scale: 1 - position * 0.08,
89
+ rotate: -4 + position * 4, // Add a fanned rotation effect
90
+ left: `${position * 20}px`, // Fan out horizontally
91
+ top: `${position * 10}px`, // Stagger vertically
92
+ zIndex: testimonials.length - position,
93
+ opacity: position === 0 ? 1 : 0.5, // Only top image is fully opaque
94
+ }}
95
+ transition={{
96
+ type: 'spring',
97
+ stiffness: 260,
98
+ damping: 25,
99
+ }}
100
+ >
101
+ <img
102
+ src={testimonial.image}
103
+ alt={testimonial.author}
104
+ className="w-full h-full object-cover"
105
+ onError={(e) => { e.target.onerror = null; e.target.src = `https://placehold.co/256x320/0f172a/a3e635?text=${testimonial.author.split(' ')[0]}`; }}
106
+ />
107
+ </motion.div>
108
+ );
109
+ })}
110
+ </div>
111
+ </div>
112
+ );
113
+ };
114
+
115
+
116
+
117
+ export default Testimonials;