apex-ui-kit 2.3.0 → 2.3.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.
@@ -11,7 +11,7 @@ import { ArrowUp, Bot, Sparkles } from "lucide-react";
11
11
 
12
12
  const DEMO_REPLIES = [
13
13
  "Sure! Wrap your section in a container with `grid` and `gap-6`, then let the cards span two columns on large screens. Want me to write the code?",
14
- "Great question. Animate only `transform` and `opacity` — they run on the GPU and never trigger layout, so the motion stays at 60fps.",
14
+ "Great question. Animate only `transform` and `opacity`, they run on the GPU and never trigger layout, so the motion stays at 60fps.",
15
15
  "Here's a quick idea: use a spring with stiffness 380 and damping 30. It feels snappy without bouncing too much.",
16
16
  ];
17
17
 
@@ -8,10 +8,10 @@ import { AnimatePresence, motion } from "framer-motion";
8
8
  */
9
9
 
10
10
  const DEFAULT_TABS = [
11
- { id: "overview", label: "Overview", content: "A quick look at everything in one place — usage, limits and the latest activity." },
11
+ { id: "overview", label: "Overview", content: "A quick look at everything in one place, usage, limits and the latest activity." },
12
12
  { id: "analytics", label: "Analytics", content: "Visitors, conversions and retention, updated every few seconds with zero setup." },
13
13
  { id: "reports", label: "Reports", content: "Export weekly PDF reports or schedule them to land in your team's inbox." },
14
- { id: "settings", label: "Settings", content: "Workspace name, members, billing and the danger zone — all in one tidy screen." },
14
+ { id: "settings", label: "Settings", content: "Workspace name, members, billing and the danger zone, all in one tidy screen." },
15
15
  ];
16
16
 
17
17
  const AnimatedTabs = ({ tabs = DEFAULT_TABS, defaultTab, variant = "pill", accentColor = "#a3e635", theme = "dark", onChange, className = "" }) => {
@@ -12,7 +12,7 @@ const DEFAULT_CARDS = [
12
12
  { icon: Sparkles, title: "Smooth", body: "Springs tuned to feel natural, never floaty." },
13
13
  { icon: Layers, title: "Composable", body: "Drop components in and style them with Tailwind." },
14
14
  { icon: Code2, title: "Yours", body: "Copy the source or add it with one CLI command." },
15
- { icon: Zap, title: "Fast", body: "Transforms and opacity only — 60fps on phones." },
15
+ { icon: Zap, title: "Fast", body: "Transforms and opacity only, 60fps on phones." },
16
16
  ];
17
17
 
18
18
  const CardSwap3D = ({ cards = DEFAULT_CARDS, delay = 3200, distanceX = 3.6, distanceY = 3.2, skew = 5, pauseOnHover = true, accentColor = "#a3e635", theme = "dark", width = 18, className = "" }) => {
@@ -16,15 +16,17 @@ const ClickSpark = ({ sparkColor = "#a3e635", sparkCount = 10, sparkRadius = 26,
16
16
 
17
17
  useEffect(() => {
18
18
  const c = canvas.current;
19
+ // Hold the element itself: the ref is cleared on unmount before cleanup runs.
20
+ const el = wrap.current;
19
21
  const resize = () => {
20
- const r = wrap.current.getBoundingClientRect();
22
+ const r = el.getBoundingClientRect();
21
23
  const dpr = Math.min(window.devicePixelRatio || 1, 2);
22
24
  c.width = r.width * dpr;
23
25
  c.height = r.height * dpr;
24
26
  c.getContext("2d").setTransform(dpr, 0, 0, dpr, 0, 0);
25
27
  };
26
28
  const ro = new ResizeObserver(resize);
27
- ro.observe(wrap.current);
29
+ ro.observe(el);
28
30
  resize();
29
31
  return () => {
30
32
  ro.disconnect();
@@ -105,6 +105,8 @@ const CubeGallery = ({ faces = DEFAULT_FACES, size = 13, autoRotate = true, spee
105
105
  className="relative h-full w-full cursor-grab touch-none active:cursor-grabbing [transform-style:preserve-3d]"
106
106
  style={{ rotateX: rotX, rotateY: rotY }}
107
107
  onPointerDown={(e) => {
108
+ // Block text selection / native drag, which cancel the drag midway.
109
+ e.preventDefault();
108
110
  e.currentTarget.setPointerCapture(e.pointerId);
109
111
  rotY.stop();
110
112
  rotX.stop();
@@ -236,6 +236,7 @@ const DotGlobe = ({
236
236
  };
237
237
 
238
238
  const onDown = (e) => {
239
+ e.preventDefault();
239
240
  drag = { x: e.clientX, yaw, lastX: e.clientX, lastT: performance.now() };
240
241
  canvas.setPointerCapture?.(e.pointerId);
241
242
  };
@@ -51,7 +51,7 @@ const DynamicIsland = ({
51
51
  accentColor = "#a3e635",
52
52
  music = { title: "Midnight City", artist: "Apex Sessions", cover: `${IK}/spiderman.png?tr=f-auto,q-80,w-200` },
53
53
  call = { name: "Tony Stark", subtitle: "Mobile", avatar: `${IK}/ironman.png?tr=f-auto,q-80,w-160` },
54
- notification = { app: "Messages", title: "Peter Parker", message: "On my way — saving the city first 🕷️" },
54
+ notification = { app: "Messages", title: "Peter Parker", message: "On my way, saving the city first 🕷️" },
55
55
  timerSeconds = 300,
56
56
  onViewChange,
57
57
  className = "",
@@ -32,6 +32,8 @@ const ElasticSlider = ({
32
32
  const [hover, setHover] = useState(false);
33
33
  const track = useRef(null);
34
34
  const drag = useRef(false);
35
+ // Latest value, readable synchronously from pointer handlers (state lags a render).
36
+ const valueRef = useRef(clamp(defaultValue));
35
37
 
36
38
  const pct = useMotionValue(((clamp(defaultValue) - min) / (max - min)) * 100);
37
39
  const overflow = useMotionValue(0); // px past an end, negative = left
@@ -44,10 +46,11 @@ const ElasticSlider = ({
44
46
 
45
47
  const commit = (raw) => {
46
48
  const v = clamp(Math.round(raw / step) * step);
47
- setValue((prev) => {
48
- if (prev !== v) onChange?.(v);
49
- return v;
50
- });
49
+ if (v !== valueRef.current) {
50
+ valueRef.current = v;
51
+ setValue(v);
52
+ onChange?.(v);
53
+ }
51
54
  return v;
52
55
  };
53
56
 
@@ -66,7 +69,7 @@ const ElasticSlider = ({
66
69
  setDragging(false);
67
70
  animate(overflow, 0, { type: "spring", stiffness: 380, damping: 12 });
68
71
  // Snap the fill to the stepped value.
69
- animate(pct, ((value - min) / (max - min)) * 100, { duration: 0.15 });
72
+ animate(pct, ((valueRef.current - min) / (max - min)) * 100, { duration: 0.12 });
70
73
  };
71
74
 
72
75
  // Keep the fill in sync for keyboard changes.
@@ -74,7 +77,7 @@ const ElasticSlider = ({
74
77
  if (!drag.current) animate(pct, ((value - min) / (max - min)) * 100, { type: "spring", stiffness: 500, damping: 40 });
75
78
  }, [value, min, max, pct]);
76
79
 
77
- const nudge = (delta) => commit(value + delta);
80
+ const nudge = (delta) => commit(valueRef.current + delta);
78
81
  const big = Math.max(step, (max - min) / 10);
79
82
  const thick = dragging || hover;
80
83
 
@@ -98,7 +101,13 @@ const ElasticSlider = ({
98
101
  onPointerLeave={() => setHover(false)}
99
102
  onPointerDown={(e) => {
100
103
  if (e.button !== 0) return;
104
+ // No text selection / native drag: those fire pointercancel mid-drag.
105
+ e.preventDefault();
106
+ e.currentTarget.focus({ preventScroll: true });
101
107
  e.currentTarget.setPointerCapture(e.pointerId);
108
+ // Stop any spring still settling from the last drag.
109
+ pct.stop();
110
+ overflow.stop();
102
111
  drag.current = true;
103
112
  setDragging(true);
104
113
  fromPointer(e.clientX);
@@ -9,7 +9,7 @@ import { Plus } from "lucide-react";
9
9
  */
10
10
 
11
11
  const DEFAULT_ITEMS = [
12
- { q: "Is ApexUI free to use?", a: "Yes. Every component is free and open source — copy it into your project or add it with one CLI command." },
12
+ { q: "Is ApexUI free to use?", a: "Yes. Every component is free and open source, copy it into your project or add it with one CLI command." },
13
13
  { q: "Does it work with Next.js?", a: "Yes. The components are plain React with Tailwind CSS, so they work in Vite, Next.js, Remix and anything else that runs React." },
14
14
  { q: "Can I change the colors?", a: "Every component takes props for its accent colors, and the Tailwind classes are right there in the code if you want to go further." },
15
15
  { q: "Do the animations respect reduced motion?", a: "Yes. Components check the visitor's reduced-motion setting and tone their movement down or switch it off." },
@@ -13,7 +13,7 @@ const IK = "https://ik.imagekit.io/ny6yinyut/apexui/assets";
13
13
  const DEFAULT_PAGES = [
14
14
  { title: "ApexUI", body: "A field guide to motion", cover: true },
15
15
  { title: "Chapter 1", body: "Start small. One spring, one easing, one idea per interaction." },
16
- { title: "Chapter 2", body: "Motion should explain — where things come from and where they go.", image: `${IK}/spiderman.png?tr=f-auto,q-80,w-500` },
16
+ { title: "Chapter 2", body: "Motion should explain, where things come from and where they go.", image: `${IK}/spiderman.png?tr=f-auto,q-80,w-500` },
17
17
  { title: "Chapter 3", body: "Keep it fast. Animate transform and opacity, never layout." },
18
18
  { title: "Chapter 4", body: "Respect reduced motion. Calm is a feature too.", image: `${IK}/doctorStrange.png?tr=f-auto,q-80,w-500` },
19
19
  { title: "Chapter 5", body: "Details compound. Tiny shadows and easing make it feel real." },
@@ -160,8 +160,10 @@ const GooeyNav = ({ items = DEFAULT_ITEMS, initialActive = 0, onChange, accentCo
160
160
  };
161
161
  sim.current = s;
162
162
 
163
+ // Hold the element itself: the ref is cleared on unmount before cleanup runs.
164
+ const barEl = bar.current;
163
165
  const resize = () => {
164
- const r = bar.current.getBoundingClientRect();
166
+ const r = barEl.getBoundingClientRect();
165
167
  s.dpr = Math.min(window.devicePixelRatio || 1, 2);
166
168
  const w = r.width + PAD * 2;
167
169
  const h = r.height + PAD * 2;
@@ -182,7 +184,7 @@ const GooeyNav = ({ items = DEFAULT_ITEMS, initialActive = 0, onChange, accentCo
182
184
  };
183
185
  resize();
184
186
  const ro = new ResizeObserver(resize);
185
- ro.observe(bar.current);
187
+ ro.observe(barEl);
186
188
 
187
189
  const dropData = new Float32Array(MAX_DROPS * 4);
188
190
  let raf = 0;
@@ -45,6 +45,9 @@ const ImageCompare = ({
45
45
  <div
46
46
  ref={ref}
47
47
  onPointerDown={(e) => {
48
+ // Block text selection / native drag, which cancel the drag midway.
49
+ e.preventDefault();
50
+ e.currentTarget.focus?.({ preventScroll: true });
48
51
  e.currentTarget.setPointerCapture(e.pointerId);
49
52
  setHeld(true);
50
53
  fromPointer(e);
@@ -81,8 +84,8 @@ const ImageCompare = ({
81
84
  aria-valuenow={Math.round(pos)}
82
85
  aria-orientation={vertical ? "vertical" : "horizontal"}
83
86
  onKeyDown={onKeyDown}
84
- className={`pointer-events-auto absolute left-1/2 top-1/2 grid h-10 w-10 -translate-x-1/2 -translate-y-1/2 place-items-center rounded-full text-black shadow-[0_6px_20px_-4px_rgba(0,0,0,0.6)] outline-none transition-transform duration-200 focus-visible:ring-4 focus-visible:ring-white/50 ${held ? "scale-110" : "group-hover:scale-105"}`}
85
- style={{ background: accentColor }}
87
+ className={`pointer-events-auto absolute left-1/2 top-1/2 grid h-10 w-10 -translate-x-1/2 -translate-y-1/2 place-items-center rounded-full bg-white text-neutral-800 shadow-[0_6px_20px_-4px_rgba(0,0,0,0.55)] outline-none transition-transform duration-200 focus-visible:ring-4 focus-visible:ring-white/50 ${held ? "scale-110" : "group-hover:scale-105"}`}
88
+ style={{ boxShadow: `0 0 0 2px ${accentColor}, 0 6px 20px -4px rgba(0,0,0,0.55)` }}
86
89
  >
87
90
  <ChevronsLeftRight className={`h-5 w-5 ${vertical ? "rotate-90" : ""}`} />
88
91
  </button>
@@ -14,7 +14,7 @@ const DefaultSecond = () => (
14
14
  <div>
15
15
  <p className="text-[0.7rem] font-semibold uppercase tracking-[0.3em] text-white/50">Wakanda</p>
16
16
  <p className="mt-2 text-3xl font-black">Black Panther</p>
17
- <p className="mt-2 text-sm text-white/60">King T&apos;Challa — protector of his people.</p>
17
+ <p className="mt-2 text-sm text-white/60">King T&apos;Challa, protector of his people.</p>
18
18
  </div>
19
19
  </div>
20
20
  );
@@ -132,7 +132,7 @@ const PointerFollower = ({
132
132
  >
133
133
  <div className="px-4 py-3 rounded-full text-[11px] overflow-hidden font-semibold shadow-[0_2px_8px_rgba(0,0,0,0.25)] border border-black/10" style={{ backgroundColor: badgeColor }}>
134
134
  <span className="text-[12px] font-medium tracking-wide whitespace-nowrap drop-shadow" style={{ color: badgeTextColor }}>
135
- {person.name} — {person.job}
135
+ {person.name} none {person.job}
136
136
  </span>
137
137
  </div>
138
138
  </div>
@@ -3,24 +3,30 @@ import { motion, useReducedMotion } from "framer-motion";
3
3
 
4
4
  /*
5
5
  * Shiny Text: a bright band of light sweeps across the text on a loop, like
6
- * light catching metal. The text keeps its own color between sweeps.
6
+ * light catching metal. The band starts and ends fully outside the text, so
7
+ * the loop is seamless — it never pauses on the last letters or jumps back.
7
8
  */
8
9
 
9
- const ShinyText = ({ text = "Shipping soon — join the waitlist", as = "span", speed = 2.8, color = "#71717a", shineColor = "#ffffff", spread = 22, disabled = false, className = "" }) => {
10
+ const ShinyText = ({ text = "Shipping soon, join the waitlist", as = "span", speed = 2.8, color = "#71717a", shineColor = "#ffffff", spread = 10, pause = 0.6, disabled = false, className = "" }) => {
10
11
  const reduced = useReducedMotion();
11
12
  const Tag = motion[as] ?? motion.span;
12
13
  const still = disabled || reduced;
14
+ // The gradient is 3× the text width with the band in its middle. Moving the
15
+ // background from 100% to 0% carries the band from fully left of the text
16
+ // to fully right of it, so both ends of the loop look identical.
17
+ const band = Math.min(16, Math.max(4, spread));
13
18
  return (
14
19
  <Tag
15
20
  className={`inline-block bg-clip-text text-transparent ${className}`}
16
21
  style={{
17
- backgroundImage: `linear-gradient(110deg, ${color} ${50 - spread}%, ${shineColor} 50%, ${color} ${50 + spread}%)`,
18
- backgroundSize: "250% 100%",
22
+ backgroundImage: `linear-gradient(100deg, ${color} ${50 - band}%, ${shineColor} 50%, ${color} ${50 + band}%)`,
23
+ backgroundSize: "300% 100%",
24
+ backgroundRepeat: "no-repeat",
19
25
  WebkitBackgroundClip: "text",
26
+ backgroundPosition: "100% 0",
20
27
  }}
21
- initial={{ backgroundPosition: "100% 0" }}
22
28
  animate={still ? { backgroundPosition: "100% 0" } : { backgroundPosition: ["100% 0", "0% 0"] }}
23
- transition={still ? undefined : { duration: speed, ease: "linear", repeat: Infinity, repeatDelay: speed * 0.25 }}
29
+ transition={still ? undefined : { duration: speed, ease: "linear", repeat: Infinity, repeatDelay: pause }}
24
30
  >
25
31
  {text}
26
32
  </Tag>
@@ -9,7 +9,7 @@ import { Check } from "lucide-react";
9
9
  */
10
10
 
11
11
  const DEFAULT_STEPS = [
12
- { title: "Account", content: <p>Welcome! Pick a username to get started — you can change it later.</p> },
12
+ { title: "Account", content: <p>Welcome! Pick a username to get started, you can change it later.</p> },
13
13
  { title: "Profile", content: <p>Add a photo and a short bio so people know who you are.</p> },
14
14
  { title: "Theme", content: <p>Choose light, dark or follow your system. Your choice syncs everywhere.</p> },
15
15
  { title: "Done", content: <p>All set. Press Complete to finish onboarding.</p> },
@@ -21,6 +21,18 @@ const variants = {
21
21
  exit: (dir) => ({ x: dir > 0 ? "-40%" : "40%", opacity: 0 }),
22
22
  };
23
23
 
24
+ function StepBody({ step, total, title, content, accentColor, dark }) {
25
+ return (
26
+ <>
27
+ <p className="text-[0.72rem] font-semibold uppercase tracking-[0.18em]" style={{ color: accentColor }}>
28
+ Step {step + 1} of {total}
29
+ </p>
30
+ <h3 className="mt-1 text-xl font-semibold">{title}</h3>
31
+ <div className={`mt-2 text-[0.92rem] leading-relaxed ${dark ? "text-white/65" : "text-neutral-600"}`}>{content}</div>
32
+ </>
33
+ );
34
+ }
35
+
24
36
  const Stepper = ({ steps = DEFAULT_STEPS, initialStep = 0, onStepChange, onComplete, accentColor = "#a3e635", theme = "dark", className = "" }) => {
25
37
  const id = useId();
26
38
  const [step, setStep] = useState(initialStep);
@@ -72,26 +84,27 @@ const Stepper = ({ steps = DEFAULT_STEPS, initialStep = 0, onStepChange, onCompl
72
84
  })}
73
85
  </div>
74
86
 
75
- <motion.div layout className="relative mt-6 overflow-hidden" transition={{ type: "spring", stiffness: 300, damping: 32 }}>
87
+ <div className="relative mt-6 grid overflow-hidden">
88
+ {/* Invisible copies of every step reserve the tallest height, so the
89
+ card and its buttons never move up and down between steps. */}
90
+ {steps.map((s, i) => (
91
+ <div key={i} aria-hidden="true" className="invisible col-start-1 row-start-1 pb-1">
92
+ <StepBody step={i} total={steps.length} title={s.title} content={s.content} accentColor={accentColor} dark={dark} />
93
+ </div>
94
+ ))}
76
95
  <AnimatePresence mode="popLayout" initial={false} custom={dir}>
77
- <motion.div key={done ? "done" : step} custom={dir} variants={variants} initial="enter" animate="center" exit="exit" transition={{ type: "spring", stiffness: 300, damping: 32 }} className="pb-1">
96
+ <motion.div key={done ? "done" : step} custom={dir} variants={variants} initial="enter" animate="center" exit="exit" transition={{ type: "spring", stiffness: 300, damping: 32 }} className="col-start-1 row-start-1 pb-1">
78
97
  {done ? (
79
98
  <div className="py-4 text-center">
80
99
  <p className="text-lg font-semibold">You are all set 🎉</p>
81
100
  <p className={`mt-1 text-sm ${dark ? "text-white/55" : "text-neutral-500"}`}>Every step is complete.</p>
82
101
  </div>
83
102
  ) : (
84
- <>
85
- <p className="text-[0.72rem] font-semibold uppercase tracking-[0.18em]" style={{ color: accentColor }}>
86
- Step {step + 1} of {steps.length}
87
- </p>
88
- <h3 className="mt-1 text-xl font-semibold">{steps[step].title}</h3>
89
- <div className={`mt-2 text-[0.92rem] leading-relaxed ${dark ? "text-white/65" : "text-neutral-600"}`}>{steps[step].content}</div>
90
- </>
103
+ <StepBody step={step} total={steps.length} title={steps[step].title} content={steps[step].content} accentColor={accentColor} dark={dark} />
91
104
  )}
92
105
  </motion.div>
93
106
  </AnimatePresence>
94
- </motion.div>
107
+ </div>
95
108
 
96
109
  <div className="mt-6 flex items-center justify-between">
97
110
  {done ? (
@@ -38,6 +38,8 @@ const TextRing3D = ({ text = "APEX UI · 3D TYPE · ", radius = 9, speed = 30, t
38
38
  className={`grid cursor-grab touch-none select-none place-items-center active:cursor-grabbing [perspective:900px] ${className}`}
39
39
  style={{ width: `${radius * 2.4}rem`, maxWidth: "100%", height: `${fontSize * 3.2 + 4}rem`, color }}
40
40
  onPointerDown={(e) => {
41
+ // Block text selection / native drag, which cancel the drag midway.
42
+ e.preventDefault();
41
43
  e.currentTarget.setPointerCapture(e.pointerId);
42
44
  drag.current = { x: e.clientX, a: angle.current, t: performance.now() };
43
45
  }}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apex-ui-kit",
3
- "version": "2.3.0",
3
+ "version": "2.3.1",
4
4
  "license": "MIT",
5
5
  "description": "Apex UI Kit - A React component library for building modern user interfaces",
6
6
  "keywords": [
@@ -1,105 +0,0 @@
1
- import React, { useState, useEffect, useRef } from 'react';
2
-
3
- // To use GSAP in this environment, we'll import it from a CDN.
4
- // In a local project: `npm install gsap`
5
- import { gsap } from 'gsap';
6
- import { Draggable } from 'gsap/Draggable';
7
-
8
- // GSAP Draggable plugin ko register karna zaroori hai
9
- gsap.registerPlugin(Draggable);
10
-
11
- // --- GSAP Drag Carousel Component ---
12
- const DragCarousel = ({ images }) => {
13
- const [activeIndex, setActiveIndex] = useState(0);
14
- const carouselRef = useRef(null);
15
- const cardsRef = useRef([]);
16
- const dragProxyRef = useRef(null);
17
- useEffect(() => {
18
- const cards = cardsRef.current;
19
- gsap.to(cards, {
20
- duration: 0.4,
21
- ease: 'power4.Out',
22
- stagger: 0.05,
23
- // Har card ke liye naya transform calculate karta hai
24
- transform: (i) => {
25
- let positionOffset = i - activeIndex;
26
- if (positionOffset > images.length / 2) {
27
- positionOffset -= images.length;
28
- } else if (positionOffset < -images.length / 2) {
29
- positionOffset += images.length;
30
- }
31
-
32
- const translateX = positionOffset * 50;
33
- const translateZ = -Math.abs(positionOffset) * 250;
34
- const scale = 1 - Math.abs(positionOffset) * 0.15;
35
-
36
- return `translateX(${translateX}%) translateZ(${translateZ}px) scale(${scale})`;
37
- },
38
- zIndex: (i) => {
39
- let positionOffset = i - activeIndex;
40
- if (positionOffset > images.length / 2) positionOffset -= images.length;
41
- if (positionOffset < -images.length / 2) positionOffset += images.length;
42
- return images.length - Math.abs(positionOffset);
43
- },
44
- opacity: (i) => {
45
- let positionOffset = i - activeIndex;
46
- if (positionOffset > images.length / 2) positionOffset -= images.length;
47
- if (positionOffset < -images.length / 2) positionOffset += images.length;
48
- return Math.abs(positionOffset) > 2 ? 0 : 1;
49
- }
50
- });
51
- }, [activeIndex, images.length]);
52
-
53
- // GSAP Draggable ko setup karta hai
54
- useEffect(() => {
55
- // BUG FIX: Draggable ab proxy element par lagaya gaya hai
56
- const draggableInstance = Draggable.create(dragProxyRef.current, {
57
- type: "x",
58
- trigger: carouselRef.current,
59
- inertia: true,
60
- cursor: 'grab',
61
- activeCursor: 'grabbing',
62
- onDragEnd: function () {
63
- gsap.to(this.target, { x: 0, duration: 0.3 });
64
- if (this.getDirection("start") === "left") {
65
- setActiveIndex(prev => (prev + 1) % images.length);
66
- } else if (this.getDirection("start") === "right") {
67
- setActiveIndex(prev => (prev - 1 + images.length) % images.length);
68
- }
69
- }
70
- });
71
-
72
- // Cleanup function
73
- return () => {
74
- if (draggableInstance && draggableInstance[0]) {
75
- draggableInstance[0].kill();
76
- }
77
- };
78
- }, [images.length]);
79
-
80
- return (
81
- <div
82
- ref={carouselRef}
83
- className="relative w-full h-[490px] lg:h-[590px] cursor-grab"
84
- style={{ perspective: '1200px', transformStyle: 'preserve-3d' }}
85
- >
86
- <div ref={dragProxyRef} className="absolute inset-0 z-20"></div>
87
-
88
- {images.map((src, index) => (
89
- <div
90
- key={index}
91
- ref={el => cardsRef.current[index] = el}
92
- className="card-item absolute h-[480px] w-[300px] lg:h-[550px] lg:w-[350px] rounded-xl overflow-hidden shadow-2xl top-[25px] -left-4 md:left-36 lg:left-70"
93
- >
94
- <img
95
- src={src}
96
- alt={`carousel-item-${index}`}
97
- className="w-full h-full object-cover pointer-events-none"
98
- />
99
- </div>
100
- ))}
101
- </div>
102
- );
103
- };
104
-
105
- export default DragCarousel;