apex-ui-kit 2.0.7 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) 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 +70 -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/OrbsBackground/FloatingOrbs.jsx +59 -59
  20. package/dist/ApexUI-Kit/templates/ParallaxCard/ParallaxCard.jsx +13 -11
  21. package/dist/ApexUI-Kit/templates/ParallaxCarousel/ParallaxCarousel.jsx +9 -9
  22. package/dist/ApexUI-Kit/templates/ParallaxProfile/ParallaxProfile.jsx +41 -38
  23. package/dist/ApexUI-Kit/templates/PointerFollower/PointerFollower.jsx +7 -7
  24. package/dist/ApexUI-Kit/templates/SmartGridCard/SmartGridCard.jsx +13 -11
  25. package/dist/ApexUI-Kit/templates/SmokeBackground/SmokeBackground.jsx +13 -2
  26. package/dist/ApexUI-Kit/templates/Testimonials/Testimonials.jsx +11 -8
  27. package/dist/ApexUI-Kit/templates/ThemeToggle/ThemeAnimation.js +40 -69
  28. package/dist/ApexUI-Kit/templates/ThemeToggle/ThemeToggle.css +1 -1
  29. package/dist/ApexUI-Kit/templates/ThemeToggle/ThemeToggle.jsx +25 -7
  30. package/dist/ApexUI-Kit/templates/ToolTip/ToolTip.jsx +71 -52
  31. package/dist/cli/index.js +24 -13
  32. package/package.json +2 -2
  33. 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 items = gsap.utils.toArray(".accordion-item");
30
- let activeItem = null;
31
- let activeMarquee = null;
32
-
33
- items.forEach((item) => {
34
- const marqueeText = item.querySelector(".marquee-text");
35
- const title = item.querySelector(".accordion-title");
36
-
37
- // Marquee text ko 2 baar duplicate karte hain for seamless loop
38
- const marqueeContent = marqueeText.innerText;
39
- 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>`;
40
- marqueeText.innerHTML = [marqueeContent, sparkleIconSVG, marqueeContent, sparkleIconSVG, marqueeContent, sparkleIconSVG, marqueeContent, sparkleIconSVG].join("");
41
-
42
- // Seamless Marquee animation
43
- const marqueeWidth = marqueeText.scrollWidth / 2;
44
- const marqueeTl = gsap.to(marqueeText, {
45
- x: -marqueeWidth,
46
- duration: marqueeWidth / 30, // Speed ko consistent rakhta hai
47
- ease: "linear",
48
- repeat: -1,
49
- }).pause();
50
-
51
- item.addEventListener("mouseenter", () => {
52
- if (activeItem === item) return;
53
-
54
- if (activeItem) {
55
- const oldTitle = activeItem.querySelector(".accordion-title");
56
- const oldMarqueeWrapper = activeItem.querySelector(".marquee-wrapper");
57
- gsap.to(oldTitle, { autoAlpha: 1, duration: 0.2, ease: "power3.inOut" });
58
- gsap.to(oldMarqueeWrapper, { autoAlpha: 0, duration: 0.2, ease: "power3.inOut" });
59
- if (activeMarquee) activeMarquee.pause();
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
- activeItem = item;
63
- activeMarquee = marqueeTl;
74
+ activeItem = item;
75
+ activeMarquee = marqueeTl;
64
76
 
65
- const marqueeWrapper = item.querySelector(".marquee-wrapper");
66
- gsap.to(title, { autoAlpha: 0, duration: 0.2, ease: "power3.inOut" });
67
- gsap.to(marqueeWrapper, { autoAlpha: 1, duration: 0.2, delay: 0.1, ease: "power3.inOut" });
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
- marqueeTl.play();
81
+ marqueeTl.play();
82
+ };
83
+ item.addEventListener("mouseenter", onEnter);
84
+ cleanups.push(() => item.removeEventListener("mouseenter", onEnter));
70
85
  });
71
- });
72
86
 
73
- containerRef.current.addEventListener("mouseleave", () => {
74
- if (!activeItem) return;
87
+ const onLeave = () => {
88
+ if (!activeItem) return;
75
89
 
76
- const activeTitle = activeItem.querySelector(".accordion-title");
77
- const activeMarqueeWrapper = activeItem.querySelector(".marquee-wrapper");
90
+ const activeTitle = activeItem.querySelector(".accordion-title");
91
+ const activeMarqueeWrapper = activeItem.querySelector(".marquee-wrapper");
78
92
 
79
- gsap.to(activeTitle, { autoAlpha: 1, duration: 0.2, delay: 0.1, ease: "power3.inOut" });
80
- gsap.to(activeMarqueeWrapper, { autoAlpha: 0, duration: 0.2, ease: "power3.inOut" });
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-screen bg-gray-900 text-white font-sans">
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
- window.addEventListener("mousemove", handleMouseMove);
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", handleMouseMove);
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
  }
@@ -1,9 +1,9 @@
1
- /* eslint-disable no-unused-vars */
2
1
  import React, { useState, useEffect, useRef } from "react";
3
2
  import { gsap } from "gsap";
4
- import { FaExternalLinkAlt, FaGithub, FaRegImage } from "react-icons/fa";
5
- const CARD_STACK_OFFSET = 32;
6
- const CARD_SCALE_STEP = 0.08;
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
7
  const AUTO_CYCLE_INTERVAL = 5000;
8
8
  export default function CardStack({
9
9
  cards = [],
@@ -17,7 +17,7 @@ export default function CardStack({
17
17
  const scrollTimeoutRef = useRef(null);
18
18
  const containerRef = useRef(null);
19
19
  useEffect(() => {
20
- if (!autoCycle || isHovered || isScrolling) return;
20
+ if (!autoCycle || isHovered || isScrolling || cards.length < 2) return;
21
21
  timeoutRef.current = setTimeout(() => {
22
22
  setActive((prev) => (prev + 1) % cards.length);
23
23
  }, cycleInterval);
@@ -40,31 +40,30 @@ export default function CardStack({
40
40
  if (!containerRef.current) return;
41
41
  const cardElements = gsap.utils.toArray(".card-item", containerRef.current);
42
42
  const tl = gsap.timeline({
43
- defaults: { duration: 0.6, ease: "elastic.out(1, 0.3)" },
43
+ defaults: { duration: 0.7, ease: "elastic.out(1, 0.45)" },
44
44
  });
45
45
  cardElements.forEach((card, i) => {
46
- const isTop = i === 0;
47
- const z = cards.length - i;
48
- const offset = i * CARD_STACK_OFFSET;
49
- const scale = 1 - i * CARD_SCALE_STEP;
50
-
51
46
  tl.to(card, {
52
- y: -offset,
53
- scale: scale,
54
- filter: isTop ? "none" : "blur(1px)",
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})`,
55
51
  opacity: 1,
56
- zIndex: z,
52
+ zIndex: cards.length - i,
57
53
  }, 0);
58
54
  });
59
55
  return () => {
60
56
  tl.kill();
61
57
  };
62
- }, [active]);
58
+ }, [active, cards.length]);
59
+
60
+ const next = () => setActive((prev) => (prev + 1) % cards.length);
63
61
  const getCardIndex = (i) => (active + i) % cards.length;
62
+
64
63
  return (
65
64
  <div
66
65
  ref={containerRef}
67
- className="relative flex justify-center items-center h-[340px] md:h-[400px] w-full max-w-lg mx-auto"
66
+ className="relative flex justify-center items-end h-[27.5rem] w-full max-w-lg mx-auto pb-2"
68
67
  onMouseEnter={() => setIsHovered(true)}
69
68
  onMouseLeave={() => setIsHovered(false)}
70
69
  >
@@ -78,24 +77,53 @@ export default function CardStack({
78
77
  return (
79
78
  <div
80
79
  key={idx}
81
- className={`card-item absolute left-0 right-0 mx-auto w-11/12 md:w-96 h-48 md:h-60 rounded-3xl shadow-xl bg-gradient-to-br ${card.color} bg-slate-950 border border-white/20 flex flex-row items-center px-3 md:px-6 py-4 cursor-pointer select-none ${isTop ? "" : "pointer-events-none"}`}
80
+ onClick={isTop ? next : undefined}
81
+ className={`card-item group absolute bottom-2 left-0 right-0 mx-auto h-[22.5rem] w-[17.5rem] 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
82
  style={{ opacity: 0 }}
83
83
  >
84
- <div className="flex-shrink-0 flex items-center justify-center w-16 h-16 md:w-24 md:h-24 lg:w-28 lg:h-28 rounded-xl overflow-hidden bg-white/10 border border-white/20 mr-3 md:mr-6">
85
- {card.image ? (<img src={card.image} alt={card.title} className="object-cover w-full h-full" />) : (<FaRegImage className="text-3xl text-white/40" />)}
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
+ )}
86
104
  </div>
87
- <div className="flex-1 flex flex-col justify-center gap-1 md:gap-2 overflow-hidden">
88
- <div className="flex items-center gap-2">
89
- <h2 className="text-base md:text-xl font-bold text-white drop-shadow truncate">{card.title}</h2>
90
- {card.github && (<a href={card.github} target="_blank" rel="noopener noreferrer" className="text-white/70 hover:text-white ml-1 flex-shrink-0"><FaGithub className="inline-block align-middle text-lg md:text-xl" /></a>)}
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>
91
122
  </div>
92
- <p className="text-xs md:text-sm text-white/60 mb-1 truncate">{card.subtitle}</p>
93
- <p className="text-xs md:text-base text-white/80 mb-1 line-clamp-2">{card.desc}</p>
94
- {card.link && (<a href={card.link} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1 text-xs md:text-sm px-2 py-1 rounded bg-blue-500/80 hover:bg-blue-600/90 text-white font-semibold shadow transition w-fit mt-1"><FaExternalLinkAlt className="inline-block mr-1 text-xs md:text-sm" />Visit</a>)}
95
123
  </div>
96
124
  </div>
97
125
  );
98
126
  })}
99
127
  </div>
100
128
  );
101
- }
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-72 rounded-2xl bg-slate-950 border border-gray-800 shadow-2xl overflow-hidden p-2 shadow-lime-700/20"
12
+ className="w-[17.5rem] 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-[30vh] w-full rounded-b-4xl rounded-t-lg overflow-hidden">
14
+ {/* Image: fixed size (in rem, like the text) so the card keeps its shape on every screen */}
15
+ <div className="h-[12.5rem] w-full rounded-b-4xl rounded-t-lg overflow-hidden">
16
16
  <motion.img
17
17
  src={image}
18
18
  alt={title}
@@ -121,7 +121,7 @@ const DragCarousel = ({ images }) => {
121
121
  <div
122
122
  key={index}
123
123
  ref={el => cardsRef.current[index] = el}
124
- className="card-item absolute h-[480px] w-[300px] lg:h-[550px] lg:w-[350px] rounded-xl overflow-hidden shadow-2xl top-[25px] -left-4 md:left-36 lg:left-70"
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]"
125
125
  >
126
126
  <img
127
127
  src={src}