apex-ui-kit 2.0.6 → 2.0.7

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.
@@ -0,0 +1,101 @@
1
+ /* eslint-disable no-unused-vars */
2
+ import React, { useState, useEffect, useRef } from "react";
3
+ 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;
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) 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.6, ease: "elastic.out(1, 0.3)" },
44
+ });
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
+ tl.to(card, {
52
+ y: -offset,
53
+ scale: scale,
54
+ filter: isTop ? "none" : "blur(1px)",
55
+ opacity: 1,
56
+ zIndex: z,
57
+ }, 0);
58
+ });
59
+ return () => {
60
+ tl.kill();
61
+ };
62
+ }, [active]);
63
+ const getCardIndex = (i) => (active + i) % cards.length;
64
+ return (
65
+ <div
66
+ ref={containerRef}
67
+ className="relative flex justify-center items-center h-[340px] md:h-[400px] w-full max-w-lg mx-auto"
68
+ onMouseEnter={() => setIsHovered(true)}
69
+ onMouseLeave={() => setIsHovered(false)}
70
+ >
71
+ {cards.map((_, i) => {
72
+ if (i >= 4) return null;
73
+
74
+ const idx = getCardIndex(i);
75
+ const isTop = i === 0;
76
+ const card = cards[idx];
77
+
78
+ return (
79
+ <div
80
+ 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"}`}
82
+ style={{ opacity: 0 }}
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" />)}
86
+ </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>)}
91
+ </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
+ </div>
96
+ </div>
97
+ );
98
+ })}
99
+ </div>
100
+ );
101
+ }
@@ -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.4,
21
- ease: 'power4.Out',
22
- stagger: 0.05,
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.15;
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
- gsap.to(this.target, { x: 0, duration: 0.3 });
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
@@ -2,18 +2,16 @@
2
2
  import React, { useState, useEffect, useRef } from 'react';
3
3
  import { motion, useMotionValue, useTransform } from 'framer-motion';
4
4
  import { ArrowLeft, ArrowRight } from 'lucide-react';
5
-
6
- // --- Card Component with Hover Parallax Effect ---
7
- const CarouselCard = ({ slide, isCenter }) => {
5
+ const CarouselCard = ({ slide, isCenter, isScrolling }) => {
8
6
  const cardRef = useRef(null);
9
7
  const x = useMotionValue(0);
10
8
  const y = useMotionValue(0);
11
9
 
12
- // Mouse move ke hisab se rotation calculate karta hai (effect ko strong banaya gaya hai)
13
10
  const rotateX = useTransform(y, [-200, 200], [15, -15]);
14
11
  const rotateY = useTransform(x, [-200, 200], [-15, 15]);
12
+
15
13
  const handleMouseMove = (event) => {
16
- if (!cardRef.current || !isCenter) return;
14
+ if (!cardRef.current || !isCenter || isScrolling) return;
17
15
  const rect = cardRef.current.getBoundingClientRect();
18
16
  x.set(event.clientX - rect.left - rect.width / 2);
19
17
  y.set(event.clientY - rect.top - rect.height / 2);
@@ -24,6 +22,13 @@ const CarouselCard = ({ slide, isCenter }) => {
24
22
  y.set(0);
25
23
  };
26
24
 
25
+ useEffect(() => {
26
+ if (isScrolling) {
27
+ x.set(0);
28
+ y.set(0);
29
+ }
30
+ }, [isScrolling, x, y]);
31
+
27
32
  return (
28
33
  <motion.div
29
34
  ref={cardRef}
@@ -36,33 +41,13 @@ const CarouselCard = ({ slide, isCenter }) => {
36
41
  }}
37
42
  onMouseMove={handleMouseMove}
38
43
  onMouseLeave={handleMouseLeave}
39
-
40
44
  >
41
45
  {/* 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
- />
46
+ <div className="absolute inset-0 w-full h-full" style={{ transformStyle: 'preserve-3d', perspective: '1500px' }}>
47
+ <img src={slide.imageUrl} alt={slide.title} className="w-full h-full object-cover group-hover:opacity-30 transition-all duration-300 ease-in-out rounded-2xl" />
48
+ <img src={slide.pngUrl} alt={slide.title} className="absolute inset-0 w-full h-full object-cover group-hover:translate-z-15 transition-all duration-300 ease-in-out rounded-2xl" />
58
49
  </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
- >
50
+ <div className="relative flex flex-col justify-end h-full text-white p-8" style={{ transformStyle: 'preserve-3d', perspective: '1500px' }}>
66
51
  <h2 className="text-3xl font-bold group-hover:translate-z-18 transition-all duration-300 ease-in-out">{slide.title}</h2>
67
52
  <p className="text-lime-400 mt-1 group-hover:translate-z-18 transition-all duration-300 ease-in-out">{slide.subtitle}</p>
68
53
  <p className="text-slate-300 mt-4 text-sm leading-relaxed group-hover:translate-z-18 transition-all duration-300 ease-in-out">{slide.text}</p>
@@ -75,37 +60,54 @@ const CarouselCard = ({ slide, isCenter }) => {
75
60
  // --- Custom Parallax Carousel Component ---
76
61
  const ParallaxCarousel = ({ slides }) => {
77
62
  const [activeIndex, setActiveIndex] = useState(0);
78
- const [isHovered, setIsHovered] = useState(false);
63
+ const animatingRef = useRef(false);
64
+ const animTimeoutRef = useRef(null);
79
65
 
66
+ const [isScrolling, setIsScrolling] = useState(false);
67
+ const scrollTimeoutRef = useRef(null);
68
+
69
+ // ✅ REMOVED: isHovered state hata diya gaya hai.
70
+
71
+ const NAV_ANIM_MS = 600;
80
72
  const handleNext = () => {
73
+ if (animatingRef.current) return;
74
+ animatingRef.current = true;
75
+ clearTimeout(animTimeoutRef.current);
81
76
  setActiveIndex((prevIndex) => (prevIndex + 1) % slides.length);
77
+ animTimeoutRef.current = setTimeout(() => {
78
+ animatingRef.current = false;
79
+ }, NAV_ANIM_MS);
82
80
  };
83
81
 
84
82
  const handlePrev = () => {
83
+ if (animatingRef.current) return;
84
+ animatingRef.current = true;
85
+ clearTimeout(animTimeoutRef.current);
85
86
  setActiveIndex((prevIndex) => (prevIndex - 1 + slides.length) % slides.length);
87
+ animTimeoutRef.current = setTimeout(() => {
88
+ animatingRef.current = false;
89
+ }, NAV_ANIM_MS);
86
90
  };
87
-
88
- // Automatic sliding ke liye
89
91
  useEffect(() => {
90
- // Agar hovered hai to interval start hi mat karo
91
- if (isHovered) {
92
- return;
93
- }
92
+ const handleScroll = () => {
93
+ setIsScrolling(true);
94
+ clearTimeout(scrollTimeoutRef.current);
95
+ scrollTimeoutRef.current = setTimeout(() => {
96
+ setIsScrolling(false);
97
+ }, 150);
98
+ };
94
99
 
95
- const interval = setInterval(() => {
96
- handleNext();
97
- }, 4000); // Har 4 second mein slide change hoga
100
+ window.addEventListener('scroll', handleScroll, { passive: true });
101
+
102
+ return () => {
103
+ window.removeEventListener('scroll', handleScroll);
104
+ clearTimeout(scrollTimeoutRef.current);
105
+ };
106
+ }, []);
98
107
 
99
- return () => clearInterval(interval); // Cleanup
100
- }, [activeIndex, isHovered]); // isHovered state change hone par effect ko firse chalayega
101
108
 
102
109
  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 */}
110
+ <div className="relative w-full flex flex-col items-center px-2">
109
111
  <div
110
112
  className="relative w-full h-[480px] portrait:h-[480px] portrait:px-0"
111
113
  style={{ perspective: '1200px', transformStyle: 'preserve-3d' }}
@@ -125,33 +127,24 @@ const ParallaxCarousel = ({ slides }) => {
125
127
  key={index}
126
128
  className={`absolute w-full h-[340px] portrait:w-full portrait:h-[480px] top-0 left-0 portrait:left-0 portrait:top-0 ${positionOffset === 0 ? '' : ' pointer-events-none'} md:w-[50%] md:h-[400px] md:top-[50px] md:left-1/2 lg:-translate-x-1/2`}
127
129
  initial={false}
128
- style={{ transformStyle: 'preserve-3d' }}
130
+ style={{ transformStyle: 'preserve-3d', willChange: 'transform, opacity' }}
129
131
  animate={{
130
132
  transform: `translateX(${positionOffset * 50}%) translateZ(${-Math.abs(positionOffset) * 250}px) scale(${1 - Math.abs(positionOffset) * 0.15})`,
131
133
  opacity: isVisible ? (1 - Math.abs(positionOffset) * 0.45) : 0,
132
134
  zIndex: slides.length - Math.abs(positionOffset),
133
135
  }}
134
- transition={{ type: 'spring', stiffness: 290, damping: 30 }}
136
+ transition={{ type: 'spring', stiffness: 160, damping: 22 }}
135
137
  >
136
- <CarouselCard slide={slide} isCenter={positionOffset === 0} />
138
+ <CarouselCard slide={slide} isCenter={positionOffset === 0} isScrolling={isScrolling} />
137
139
  </motion.div>
138
140
  );
139
141
  })}
140
142
  </div>
141
143
 
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
- >
144
+ <motion.button onClick={handlePrev} className="absolute -left-10 lg:left-0 top-1/2 -translate-y-1/2 z-30 p-3 rounded-full text-slate-400 hover:text-lime-400 transition-colors" whileTap={{ scale: 0.9 }}>
148
145
  <ArrowLeft size={32} />
149
146
  </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
- >
147
+ <motion.button onClick={handleNext} className="absolute -right-10 lg:right-0 top-1/2 -translate-y-1/2 z-30 p-3 rounded-full text-slate-400 hover:text-lime-400 transition-colors" whileTap={{ scale: 0.9 }}>
155
148
  <ArrowRight size={32} />
156
149
  </motion.button>
157
150
  </div>
@@ -6,25 +6,24 @@ const GitHubIcon = () => (<svg width="24" height="24" fill="none" stroke="curren
6
6
  const LinkedInIcon = () => (<svg width="24" height="24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" viewBox="0 0 24 24"><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z" /><rect x="2" y="9" width="4" height="12" /><circle cx="4" cy="4" r="2" /></svg>);
7
7
  const TwitterIcon = () => (<BsTwitterX size={24} />);
8
8
  const InstagramIcon = () => (<svg width="24" height="24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" viewBox="0 0 24 24"><rect x="2" y="2" width="20" height="20" rx="5" ry="5" /><path d="M16 11.37A4 4 0 1112.63 8 4 4 0 0116 11.37z" /><line x1="17.5" y1="6.5" x2="17.5" y2="6.5" /></svg>);
9
- const ParallaxProfileCard = ({ profile }) => {
10
- const defaultProfile = {
11
- avatarUrl: "/assets/spiderman.png",
12
- name: "Spider Man",
13
- username: "spider_man",
14
- dob: "10-08-2001",
15
- title: "PhotoGrapher",
16
- status: "Available for Hire",
17
- socialLinks: {
18
- github: "https://github.com/",
19
- linkedin: "https://www.linkedin.com/",
20
- twitter: "https://twitter.com/",
21
- instagram: "https://www.instagram.com/",
22
- },
23
- // Button actions
24
- onHireClick: () => alert("Hire Me button clicked!"),
25
- onCvClick: () => alert("CV button clicked!"),
26
- };
9
+ const defaultProfile = {
10
+ avatarUrl: "/assets/spiderman.png",
11
+ name: "Spider Man",
12
+ username: "spider_man",
13
+ dob: "10-08-2001",
14
+ title: "PhotoGrapher",
15
+ status: "Available for Hire",
16
+ socialLinks: {
17
+ github: "https://github.com/",
18
+ linkedin: "https://www.linkedin.com/",
19
+ twitter: "https://twitter.com/",
20
+ instagram: "https://www.instagram.com/",
21
+ },
22
+ onHireClick: () => alert("Hire Me button clicked!"),
23
+ onCvClick: () => alert("CV button clicked!"),
24
+ };
27
25
 
26
+ const ParallaxProfileCard = ({ profile }) => {
28
27
  const {
29
28
  avatarUrl, name, username, dob, title, status, socialLinks, onHireClick, onCvClick
30
29
  } = profile || defaultProfile;
@@ -104,4 +103,16 @@ const ParallaxProfileCard = ({ profile }) => {
104
103
  );
105
104
  };
106
105
 
107
- export default ParallaxProfileCard;
106
+ const ParallaxProfile = ({ profiles = [defaultProfile], className = '' }) => {
107
+ const safe = Array.isArray(profiles) && profiles.length > 0 ? profiles : [defaultProfile];
108
+ return (
109
+ <div className={`flex flex-wrap gap-6 justify-center ${className}`}>
110
+ {safe.map((p, i) => (
111
+ <ParallaxProfileCard key={p.id || p.username || i} profile={p} />
112
+ ))}
113
+ </div>
114
+ );
115
+ };
116
+
117
+ export { ParallaxProfileCard };
118
+ export default ParallaxProfile;
@@ -1,6 +1,6 @@
1
1
  import React, { useRef, useEffect, useState } from "react";
2
2
 
3
- const people = [
3
+ const defaultPeople = [
4
4
  { id: 1, name: "Captain America", job: "Leader of the Avengers", img: "/assets/captainamerica.png" },
5
5
  { id: 2, name: "Doctor Strange", job: "Sorcerer Supreme", img: "/assets/doctorStrange.png" },
6
6
  { id: 3, name: "Iron Man", job: "Leader Of Stark Industries", img: "/assets/ironman.png" },
@@ -9,7 +9,13 @@ const people = [
9
9
  { id: 6, name: "Thanos", job: "The Mad Titan", img: "/assets/thanos.png" },
10
10
  ];
11
11
 
12
- const PointerFollower = ({ cursorColor = "#fff", interval = 3000, badgeColor = "#fff", badgeTextColor = "#212121" }) => {
12
+ const PointerFollower = ({
13
+ cursorColor = "#fff",
14
+ interval = 3000,
15
+ badgeColor = "#fff",
16
+ badgeTextColor = "#212121",
17
+ people = defaultPeople,
18
+ }) => {
13
19
  const containerRef = useRef(null);
14
20
  const [pos, setPos] = useState({ x: 0, y: 0 });
15
21
  const [hovered, setHovered] = useState(false);
@@ -20,11 +26,14 @@ const PointerFollower = ({ cursorColor = "#fff", interval = 3000, badgeColor = "
20
26
  if (hovered) return;
21
27
 
22
28
  const autoInterval = setInterval(() => {
23
- setActiveIndex((prev) => (prev + 1) % people.length);
29
+ setActiveIndex((prev) => {
30
+ if (!people || people.length === 0) return 0;
31
+ return (prev + 1) % people.length;
32
+ });
24
33
  }, interval);
25
34
 
26
35
  return () => clearInterval(autoInterval);
27
- }, [hovered, interval]);
36
+ }, [hovered, interval, people]);
28
37
 
29
38
  // 🖱️ Mouse tracking
30
39
  useEffect(() => {
@@ -53,13 +62,24 @@ const PointerFollower = ({ cursorColor = "#fff", interval = 3000, badgeColor = "
53
62
  };
54
63
  }, []);
55
64
 
56
- const person = people[activeIndex]; // ✅ SYNCED HERE
65
+ // Guard against empty people array
66
+ const safePeople = Array.isArray(people) && people.length > 0 ? people : defaultPeople;
67
+ const person = safePeople[activeIndex % safePeople.length]; // ✅ SYNCED HERE
68
+
69
+ // If the provided people list changes, ensure activeIndex is valid
70
+ useEffect(() => {
71
+ if (!Array.isArray(people) || people.length === 0) {
72
+ setActiveIndex(0);
73
+ return;
74
+ }
75
+ setActiveIndex((idx) => idx % people.length);
76
+ }, [people]);
57
77
 
58
78
  return (
59
79
  <div className="relative w-full h-fit flex items-center justify-center">
60
80
  <div ref={containerRef} className="absolute flex items-center justify-center w-64 h-66 cursor-none">
61
81
  {/* 🔁 Image stack with auto fade */}
62
- {people.map((p, i) => (
82
+ {safePeople.map((p, i) => (
63
83
  <img
64
84
  key={i}
65
85
  src={p.img}
@@ -67,7 +87,7 @@ const PointerFollower = ({ cursorColor = "#fff", interval = 3000, badgeColor = "
67
87
  className={`absolute w-full h-full object-cover rounded-xl transition-opacity duration-1000 ease-in-out border border-white/10`}
68
88
  style={{
69
89
  transform: `translate(${i * -2}px, ${i * -2}px)`,
70
- zIndex: people.length - i,
90
+ zIndex: safePeople.length - i,
71
91
  opacity: activeIndex === i ? 1 : 0,
72
92
  }}
73
93
  />
@@ -87,30 +87,37 @@ void main() {
87
87
 
88
88
 
89
89
  export const SmokeBackground = ({
90
- color = "#ffffff",
90
+ color = "#00c950",
91
91
  speed = 1,
92
92
  direction = "forward",
93
93
  scale = 1,
94
94
  opacity = 1,
95
95
  mouseInteractive = true,
96
96
  className = "",
97
+ maxWidth = 'max-w-4xl',
97
98
  }) => {
98
99
  const containerRef = useRef(null);
99
100
  const mousePos = useRef({ x: 0, y: 0 });
100
101
 
101
102
  useEffect(() => {
102
- if (!containerRef.current) return;
103
+ const mountNode = containerRef.current;
104
+ if (!mountNode) return;
103
105
 
104
106
  const useCustomColor = color ? 1.0 : 0.0;
105
107
  const customColorRgb = color ? hexToRgb(color) : [1, 1, 1];
106
108
 
107
109
  let directionMultiplier = direction === "backward" ? -1.0 : 1.0;
108
110
 
111
+ // cap DPR depending on mount width to avoid heavy pixel loads on small devices
112
+ const mountWidth = mountNode.clientWidth || window.innerWidth;
113
+ const dprCap = mountWidth > 1200 ? 1.8 : (mountWidth > 768 ? 1.4 : 1.0);
114
+ const dpr = Math.min(window.devicePixelRatio || 1, dprCap);
115
+
109
116
  const renderer = new Renderer({
110
117
  webgl: 2,
111
118
  alpha: true,
112
119
  antialias: false,
113
- dpr: Math.min(window.devicePixelRatio || 1, 1.8),
120
+ dpr,
114
121
  });
115
122
 
116
123
  const gl = renderer.gl;
@@ -118,7 +125,7 @@ export const SmokeBackground = ({
118
125
  canvas.style.display = "block";
119
126
  canvas.style.width = "100%";
120
127
  canvas.style.height = "100%";
121
- containerRef.current.appendChild(canvas);
128
+ mountNode.appendChild(canvas);
122
129
 
123
130
  const geometry = new Triangle(gl);
124
131
 
@@ -145,17 +152,17 @@ export const SmokeBackground = ({
145
152
  let mouseNeedsUpdate = false;
146
153
  const handleMouseMove = (e) => {
147
154
  if (!mouseInteractive) return;
148
- const rect = containerRef.current.getBoundingClientRect();
155
+ const rect = mountNode.getBoundingClientRect();
149
156
  mousePos.current.x = e.clientX - rect.left;
150
157
  mousePos.current.y = rect.height - (e.clientY - rect.top);
151
158
  mouseNeedsUpdate = true;
152
159
  };
153
160
  if (mouseInteractive) {
154
- containerRef.current.addEventListener("mousemove", handleMouseMove);
161
+ mountNode.addEventListener("mousemove", handleMouseMove);
155
162
  }
156
163
 
157
164
  const setSize = () => {
158
- const rect = containerRef.current.getBoundingClientRect();
165
+ const rect = mountNode.getBoundingClientRect();
159
166
  const width = Math.max(1, Math.floor(rect.width));
160
167
  const height = Math.max(1, Math.floor(rect.height));
161
168
  renderer.setSize(width, height);
@@ -167,7 +174,7 @@ export const SmokeBackground = ({
167
174
  const ro = new ResizeObserver(() => {
168
175
  requestAnimationFrame(setSize);
169
176
  });
170
- ro.observe(containerRef.current);
177
+ ro.observe(mountNode);
171
178
  setSize();
172
179
 
173
180
  let raf = 0;
@@ -196,20 +203,21 @@ export const SmokeBackground = ({
196
203
  return () => {
197
204
  cancelAnimationFrame(raf);
198
205
  ro.disconnect();
199
- if (mouseInteractive && containerRef.current) {
200
- containerRef.current.removeEventListener("mousemove", handleMouseMove);
206
+ if (mouseInteractive && mountNode) {
207
+ mountNode.removeEventListener("mousemove", handleMouseMove);
201
208
  }
202
209
  try {
203
- containerRef.current?.removeChild(canvas);
210
+ mountNode.removeChild(canvas);
204
211
  } catch (error) { console.error("Error removing canvas:", error); }
205
212
  };
206
213
  }, [color, speed, direction, scale, opacity, mouseInteractive, className]);
207
214
 
208
215
  return (
209
- <div
210
- ref={containerRef}
211
- className={`w-screen lg:h-full h-[80vh] overflow-hidden relative ${className}`}
212
- />
216
+ <div className={`w-full ${className}`}>
217
+ <div className={`mx-auto w-full ${maxWidth} h-[100vh] lg:h-[90vh] overflow-hidden relative`}>
218
+ <div ref={containerRef} className="absolute inset-0 w-full h-full" />
219
+ </div>
220
+ </div>
213
221
  );
214
222
  };
215
223
 
@@ -95,6 +95,8 @@
95
95
  --color-template-border: #fdc700;
96
96
  --color-template-text: #fdc700;
97
97
  --color-scan: #460809;
98
+ --selection-bg: #460809;
99
+ --selection-text: #fdc700;
98
100
 
99
101
  /* Hero Component */
100
102
  --right-color-bg: #e5c287;
@@ -246,6 +248,8 @@
246
248
  --color-btn-hover-text: #d9f99d;
247
249
  --color-stroke-color: #9ae600;
248
250
  --color-scan: #9ae600;
251
+ --selection-bg: #9ae600;
252
+ --selection-text: #1e2939;
249
253
  /* Hero Component */
250
254
  --color-stroke-color: #9ae600;
251
255
  --color-template: #101828;
package/dist/cli/index.js CHANGED
@@ -21,7 +21,7 @@ const componentMap = {
21
21
  "basic-card": "Cards",
22
22
  "edge-glow-from": "EdgeGlowFrom",
23
23
  "glare-card": "GlareCard",
24
- "glass-card-stack": "GlassCardStack",
24
+ "card-stack": "CardStack",
25
25
  "glass-profile": "GlassProfile",
26
26
  "hover-text": "HoverText",
27
27
  "hyper-card": "HyperCard",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apex-ui-kit",
3
- "version": "2.0.6",
3
+ "version": "2.0.7",
4
4
  "license": "MIT",
5
5
  "description": "Apex UI Kit - A React component library for building modern user interfaces",
6
6
  "keywords": [
@@ -1,111 +0,0 @@
1
- /* eslint-disable no-unused-vars */
2
- import React, { useState, useEffect, useRef } from "react";
3
- import { motion, AnimatePresence } from "framer-motion";
4
- import { FaExternalLinkAlt, FaGithub, FaRegImage } from "react-icons/fa";
5
- const CARD_STACK_OFFSET = 32;
6
- const CARD_SCALE_STEP = 0.08;
7
- const AUTO_CYCLE_INTERVAL = 5000;
8
- export default function GlassCardStack({
9
- cards = [],
10
- autoCycle = true,
11
- cycleInterval = AUTO_CYCLE_INTERVAL,
12
- }) {
13
- const [active, setActive] = useState(0);
14
- const [paused, setPaused] = useState(false);
15
- const timeoutRef = useRef();
16
-
17
- useEffect(() => {
18
- if (!autoCycle || paused) return;
19
- timeoutRef.current = setTimeout(() => {
20
- setActive((prev) => (prev + 1) % cards.length);
21
- }, cycleInterval);
22
- return () => clearTimeout(timeoutRef.current);
23
- }, [active, paused, autoCycle, cycleInterval, cards.length]);
24
-
25
- // For cycling animation
26
- const getCardIndex = (i) => (active + i) % cards.length;
27
-
28
- return (
29
- <div
30
- className="relative flex justify-center items-center h-[340px] md:h-[400px] w-full max-w-lg mx-auto select-none"
31
- onMouseEnter={() => setPaused(true)}
32
- onMouseLeave={() => setPaused(false)}
33
- >
34
- {cards.map((_, i) => {
35
- // Stack order: 0 is top, 1 is next, etc.
36
- const idx = getCardIndex(i);
37
- const isTop = i === 0;
38
- const z = cards.length - i;
39
- const offset = i * CARD_STACK_OFFSET;
40
- const scale = 1 - i * CARD_SCALE_STEP;
41
- const card = cards[idx];
42
- return (
43
- <AnimatePresence key={idx}>
44
- <motion.div
45
- key={idx}
46
- initial={{
47
- y: 60,
48
- opacity: 0,
49
- scale: scale * 0.95,
50
- }}
51
- animate={{
52
- y: -offset,
53
- opacity: 1,
54
- scale,
55
- zIndex: z,
56
- filter: isTop ? "none" : "blur(2px)",
57
- }}
58
- exit={{
59
- y: -100,
60
- opacity: 0,
61
- scale: scale * 0.95,
62
- }}
63
- transition={{ type: "spring", stiffness: 300, damping: 30 }}
64
- className={`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} backdrop-blur-lg border border-white/20 flex flex-row items-center px-3 md:px-6 py-4 cursor-pointer select-none ${isTop ? "" : "pointer-events-none"}`}
65
- style={{ zIndex: z }}
66
- >
67
- {/* Left: Responsive Image */}
68
- <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">
69
- {card.image ? (
70
- <img
71
- src={card.image}
72
- alt={card.title}
73
- className="object-cover w-full h-full"
74
- />
75
- ) : (
76
- <FaRegImage className="text-3xl text-white/40" />
77
- )}
78
- </div>
79
- {/* Right: Info */}
80
- <div className="flex-1 flex flex-col justify-center gap-1 md:gap-2">
81
- <div className="flex items-center gap-2">
82
- <h2 className="text-base md:text-xl font-bold text-white drop-shadow">
83
- {card.title}
84
- </h2>
85
- {card.github && (
86
- <a href={card.github} target="_blank" rel="noopener noreferrer" className="text-white/70 hover:text-white ml-1">
87
- <FaGithub className="inline-block align-middle text-lg md:text-xl" />
88
- </a>
89
- )}
90
- </div>
91
- <p className="text-xs md:text-sm text-white/60 mb-1">{card.subtitle}</p>
92
- <p className="text-xs md:text-base text-white/80 mb-1 line-clamp-2">{card.desc}</p>
93
- {card.link && (
94
- <a
95
- href={card.link}
96
- target="_blank"
97
- rel="noopener noreferrer"
98
- 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"
99
- >
100
- <FaExternalLinkAlt className="inline-block mr-1 text-xs md:text-sm" />
101
- Visit
102
- </a>
103
- )}
104
- </div>
105
- </motion.div>
106
- </AnimatePresence>
107
- );
108
- })}
109
- </div>
110
- );
111
- }