apex-ui-kit 2.6.0 → 2.7.0

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.
@@ -1,161 +1,340 @@
1
- import React, { useLayoutEffect, useRef } from 'react';
2
- // GSAP ko seedhe CDN se import kiya gaya hai taaki yeh browser mein chale
3
- import gsap from "gsap";
4
- import './AccordionMarquee.css'
5
- // Accordion ka data
6
- const accordionData = [
7
- {
8
- title: "Cybernetic Dreams",
9
- text: "Explore the Future",
10
- },
11
- {
12
- title: "Quantum Leap",
13
- text: "Journey Through Spacetime",
14
- },
15
- {
16
- title: "Neural Networks",
17
- text: "Unravel Complexity",
18
- },
19
- {
20
- title: "Digital Odyssey",
21
- text: "Voyage into the Digital World",
22
- },
23
- ];
24
-
25
- const AccordionMarquee = ({bgColor='#bbf451', textColor='#27272a'}) => {
26
- const containerRef = useRef(null);
27
-
28
- useLayoutEffect(() => {
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
- }
73
-
74
- activeItem = item;
75
- activeMarquee = marqueeTl;
76
-
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 });
80
-
81
- marqueeTl.play();
82
- };
83
- item.addEventListener("mouseenter", onEnter);
84
- cleanups.push(() => item.removeEventListener("mouseenter", onEnter));
85
- });
86
-
87
- const onLeave = () => {
88
- if (!activeItem) return;
89
-
90
- const activeTitle = activeItem.querySelector(".accordion-title");
91
- const activeMarqueeWrapper = activeItem.querySelector(".marquee-wrapper");
92
-
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 });
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
- };
110
- }, []);
111
-
112
- return (
113
- <>
114
- <style>
115
- {`
116
- @keyframes sparkle-pulse {
117
- 0%, 100% {
118
- stroke: #000;
119
- stroke-width: 0.5;
120
- }
121
- 50% {
122
- stroke: #444;
123
- stroke-width: 1;
124
- }
125
- }
126
- .sparkle-icon {
127
- animation: sparkle-pulse 2.5s ease-in-out infinite;
128
- }
129
- `}
130
- </style>
131
- <div className="flex items-center justify-center w-full bg-gray-900 text-white font-sans">
132
- <div ref={containerRef} className="flex flex-col w-full h-[80vh] md:h-[70vh]">
133
- {accordionData.map((item, index) => (
134
- <React.Fragment key={index}>
135
- <div className="accordion-item relative w-full h-40 overflow-hidden cursor-pointer flex items-center justify-center bg-gray-800">
136
- <div className="relative z-10 w-full text-center px-4">
137
- <h2 className="accordion-title text-2xl sm:text-3xl lg:text-5xl font-extrabold uppercase tracking-wider whitespace-nowrap" style={{ textShadow: '2px 2px 8px rgba(0,0,0,0.7)' }}>
138
- {item.title}
139
- </h2>
140
- <div className="marquee-wrapper absolute inset-0 w-full h-20 flex items-center overflow-hidden opacity-0" style={{backgroundColor: bgColor}}>
141
- <p className="marquee-text text-4xl sm:text-5xl md:text-6xl font-bold whitespace-nowrap flex items-center"
142
- style={{ textShadow: '1px 1px 3px rgba(0,0,0,0.2)', fontFamily: "Righteous, sans-serif", color: textColor }}
143
- >
144
- {item.text}
145
- </p>
146
- </div>
147
- </div>
148
- </div>
149
- {index < accordionData.length - 1 && (
150
- <hr className="w-full border-t-2 border-gray-700 my-0" />
151
- )}
152
- </React.Fragment>
153
- ))}
154
- </div>
155
- </div>
156
- </>
157
- );
158
- };
159
-
160
- export default AccordionMarquee;
161
-
1
+ import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
2
+
3
+ /*
4
+ * Accordion Marquee: a list of big rows with one colored band that glides
5
+ * from row to row under the pointer. The band's two edges ride separate
6
+ * springs (the leading edge is quicker), so it stretches like liquid as it
7
+ * travels and snaps back when it lands; it also leans a little toward the
8
+ * pointer. Inside the band each row's marquee scrolls, and moving the mouse
9
+ * sideways speeds it up or turns it around. Works with touch (tap a row)
10
+ * and keyboard (Tab through the rows).
11
+ */
12
+
13
+ const IK = "https://ik.imagekit.io/ny6yinyut/apexui/assets/componentsimage";
14
+ const shot = (n) => `${IK}/${n}.png?tr=f-auto,q-70,w-420`;
15
+ const DEFAULT_ITEMS = [
16
+ { title: "Cybernetic Dreams", text: "Explore the future", image: shot("galaxy-3d") },
17
+ { title: "Quantum Leap", text: "Journey through spacetime", image: shot("warp-tunnel") },
18
+ { title: "Neural Networks", text: "Unravel complexity", image: shot("particle-morph") },
19
+ { title: "Digital Odyssey", text: "Voyage into the digital world", image: shot("retro-grid-3d") },
20
+ ];
21
+
22
+ const clamp = (v, a, b) => Math.min(b, Math.max(a, v));
23
+ const SHAPES = { pill: "rounded-full aspect-[2.2/1]", rounded: "rounded-[14cqh] aspect-[3/2]", circle: "rounded-full aspect-square" };
24
+
25
+ const Part = ({ item, shape }) => (
26
+ <div className="flex h-full shrink-0 items-center">
27
+ <span className="whitespace-nowrap px-[0.45em] text-[clamp(0.95rem,min(36cqh,7.5cqw),6rem)] font-semibold leading-none tracking-[-0.02em]">{item.text ?? item.title}</span>
28
+ {item.image ? <span className={`mx-[3cqh] block h-[58cqh] shrink-0 bg-cover bg-center ${shape}`} style={{ backgroundImage: `url(${item.image})` }} /> : <span className="mx-[0.3em] text-[clamp(0.8rem,24cqh,3rem)] leading-none opacity-70">✦</span>}
29
+ </div>
30
+ );
31
+
32
+ const AccordionMarquee = ({
33
+ items = DEFAULT_ITEMS,
34
+ speed = 70,
35
+ direction = "left",
36
+ cursorBoost = 0.5,
37
+ duration = 0.5,
38
+ stretch = 0.6,
39
+ magnet = 0.2,
40
+ bgColor = "#bbf451",
41
+ textColor = "#18181b",
42
+ menuBg = "#0f0f12",
43
+ menuColor = "#fafafa",
44
+ lineColor = "rgba(255,255,255,0.14)",
45
+ inset = 0,
46
+ radius = 0,
47
+ imageShape = "pill",
48
+ showIndex = false,
49
+ dimOthers = false,
50
+ intro = true,
51
+ height = "32rem",
52
+ onSelect,
53
+ className = "",
54
+ }) => {
55
+ const root = useRef(null);
56
+ const list = useRef(null);
57
+ const bandRef = useRef(null);
58
+ const rowEls = useRef([]);
59
+ const strips = useRef([]);
60
+ const [copies, setCopies] = useState([]);
61
+ const live = useRef({});
62
+ live.current = { speed, direction, cursorBoost, duration, stretch, magnet, inset, radius, dimOthers, onSelect };
63
+ const engine = useRef(null);
64
+ const list_ = items.length ? items : DEFAULT_ITEMS;
65
+
66
+ useLayoutEffect(() => {
67
+ const nav = root.current;
68
+ const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
69
+ const rows = [];
70
+ const runs = [];
71
+ const b = { on: false, index: -1, top: 0, bot: 0, vTop: 0, vBot: 0, tTop: 0, tBot: 0, lead: 1 };
72
+ const cur = { x: 0, y: null, t: 0, vx: 0 };
73
+ let raf = 0;
74
+ let last = 0;
75
+ let visible = true;
76
+
77
+ const mark = () => {
78
+ rowEls.current.forEach((li, i) => {
79
+ if (!li) return;
80
+ li.toggleAttribute("data-active", i === b.index);
81
+ li.toggleAttribute("data-dim", live.current.dimOthers && b.index >= 0 && i !== b.index);
82
+ });
83
+ };
84
+
85
+ const paint = () => {
86
+ const band = bandRef.current;
87
+ if (!band) return;
88
+ const H = list.current?.clientHeight ?? 0;
89
+ const o = live.current;
90
+ const tall = b.bot - b.top;
91
+ if (!b.on || tall < 0.3) {
92
+ band.style.visibility = "hidden";
93
+ return;
94
+ }
95
+ const side = clamp(o.inset, 0, 40);
96
+ const r = Math.min(o.radius, tall / 2);
97
+ band.style.visibility = "visible";
98
+ band.style.clipPath = `inset(${b.top.toFixed(2)}px ${side}px ${(H - b.bot).toFixed(2)}px ${side}px round ${r.toFixed(2)}px)`;
99
+ runs.forEach((run, i) => {
100
+ const track = strips.current[i]?.firstElementChild;
101
+ if (track && run.spin > 0) track.style.transform = `translate3d(${run.x.toFixed(2)}px,0,0)`;
102
+ });
103
+ };
104
+
105
+ const frame = (now) => {
106
+ raf = 0;
107
+ const o = live.current;
108
+ const dt = last ? Math.min(0.05, (now - last) / 1000) : 1 / 60;
109
+ last = now;
110
+ let busy = false;
111
+
112
+ if (b.on) {
113
+ const row = rows[b.index];
114
+ if (row) {
115
+ const ins = Math.min(o.inset, row.h / 2 - 2);
116
+ const lean = cur.y === null || reduced ? 0 : clamp((cur.y - (row.top + row.h / 2)) * o.magnet, -row.h * 0.3, row.h * 0.3);
117
+ b.tTop = row.top + ins + lean;
118
+ b.tBot = row.top + row.h - ins + lean;
119
+ }
120
+ const move = (b.tTop + b.tBot) / 2 - (b.top + b.bot) / 2;
121
+ if (Math.abs(move) > 1) b.lead = Math.sign(move);
122
+ if (reduced) {
123
+ b.top = b.tTop;
124
+ b.bot = b.tBot;
125
+ b.vTop = b.vBot = 0;
126
+ } else {
127
+ // Two springs: the edge heading the move is stiffer, the trailing one lags.
128
+ const w = (Math.PI * 2) / Math.max(0.1, o.duration);
129
+ const quick = w * (1 + o.stretch);
130
+ const lazy = w * (1 - 0.55 * o.stretch);
131
+ const wTop = b.lead > 0 ? lazy : quick;
132
+ const wBot = b.lead > 0 ? quick : lazy;
133
+ const n = Math.ceil(dt * 240);
134
+ const h = dt / n;
135
+ for (let k = 0; k < n; k++) {
136
+ b.vTop += (-wTop * wTop * (b.top - b.tTop) - 1.8 * wTop * b.vTop) * h;
137
+ b.vBot += (-wBot * wBot * (b.bot - b.tBot) - 1.8 * wBot * b.vBot) * h;
138
+ b.top += b.vTop * h;
139
+ b.bot += b.vBot * h;
140
+ if (b.bot < b.top) {
141
+ const mid = (b.top + b.bot) / 2;
142
+ const v = (b.vTop + b.vBot) / 2;
143
+ b.top = b.bot = mid;
144
+ b.vTop = b.vBot = v;
145
+ }
146
+ }
147
+ }
148
+ const still = Math.abs(b.top - b.tTop) < 0.1 && Math.abs(b.bot - b.tBot) < 0.1 && Math.abs(b.vTop) + Math.abs(b.vBot) < 3;
149
+ if (still) {
150
+ b.top = b.tTop;
151
+ b.bot = b.tBot;
152
+ b.vTop = b.vBot = 0;
153
+ if (b.index < 0) b.on = false;
154
+ } else busy = true;
155
+ }
156
+
157
+ // Marquees: only the rows the band covers run, easing in and out.
158
+ cur.vx *= Math.exp(-dt / 0.3);
159
+ const push = clamp(cur.vx * o.cursorBoost * 0.5, -1500, 1500);
160
+ runs.forEach((run, i) => {
161
+ const row = rows[i];
162
+ const covered = b.on && row && Math.min(b.bot, row.top + row.h) - Math.max(b.top, row.top) > row.h * 0.04;
163
+ const goal = covered ? 1 : 0;
164
+ run.spin += (goal - run.spin) * (1 - Math.exp(-dt / 0.3));
165
+ if (Math.abs(goal - run.spin) < 0.003) run.spin = goal;
166
+ if (!run.spin) return;
167
+ busy = true;
168
+ if (!run.part || reduced) return;
169
+ const way = o.direction === "right" ? 1 : o.direction === "alternate" && i % 2 ? 1 : -1;
170
+ run.x += (way * o.speed + push) * run.spin * dt;
171
+ run.x = (((run.x % run.part) + run.part) % run.part) - run.part;
172
+ });
173
+
174
+ paint();
175
+ if (busy && visible) raf = requestAnimationFrame(frame);
176
+ else last = 0;
177
+ };
178
+ const wake = () => {
179
+ if (!raf && visible) raf = requestAnimationFrame(frame);
180
+ };
181
+
182
+ const measure = () => {
183
+ rows.length = 0;
184
+ rowEls.current.forEach((li, i) => {
185
+ if (!li) return;
186
+ rows[i] = { top: li.offsetTop, h: li.offsetHeight };
187
+ const strip = strips.current[i];
188
+ if (strip) {
189
+ strip.style.top = `${li.offsetTop}px`;
190
+ strip.style.height = `${li.offsetHeight}px`;
191
+ }
192
+ runs[i] = runs[i] || { x: 0, part: 0, spin: 0 };
193
+ });
194
+ const need = rows.map((row, i) => {
195
+ const part = strips.current[i]?.firstElementChild?.firstElementChild;
196
+ const pw = part?.getBoundingClientRect().width ?? 0;
197
+ const run = runs[i];
198
+ if (pw > 0) {
199
+ run.part = pw;
200
+ run.x = (((run.x % pw) + pw) % pw) - pw;
201
+ }
202
+ return pw > 0 ? Math.ceil((list.current?.clientWidth ?? 0) / pw) + 2 : 3;
203
+ });
204
+ setCopies((c) => (c.length === need.length && c.every((v, i) => v === need[i]) ? c : need));
205
+ if (b.on && b.index >= 0) wake();
206
+ paint();
207
+ };
208
+
209
+ const rowAt = (y) => rows.findIndex((r) => r && y >= r.top && y < r.top + r.h);
210
+ const goTo = (i, fromY) => {
211
+ const row = rows[i];
212
+ if (!row) return;
213
+ if (!b.on) {
214
+ // Grow out of the edge the pointer came in from.
215
+ const start = fromY !== undefined && fromY > row.top + row.h / 2 ? row.top + row.h : row.top;
216
+ b.top = b.bot = start;
217
+ b.vTop = b.vBot = 0;
218
+ b.on = true;
219
+ }
220
+ b.index = i;
221
+ mark();
222
+ wake();
223
+ };
224
+ const release = (toward) => {
225
+ if (!b.on) return;
226
+ const p = toward === "top" ? b.tTop : toward === "bottom" ? b.tBot : (b.tTop + b.tBot) / 2;
227
+ b.tTop = b.tBot = p;
228
+ b.index = -1;
229
+ cur.y = null;
230
+ mark();
231
+ wake();
232
+ };
233
+
234
+ const ul = list.current;
235
+ const localY = (e) => e.clientY - ul.getBoundingClientRect().top;
236
+ const onMove = (e) => {
237
+ if (e.pointerType === "touch") return;
238
+ const now = performance.now();
239
+ const gap = (now - cur.t) / 1000;
240
+ if (gap > 0 && gap < 0.1) cur.vx += ((e.clientX - cur.x) / gap - cur.vx) * (1 - Math.exp(-gap / 0.08));
241
+ cur.x = e.clientX;
242
+ cur.t = now;
243
+ cur.y = localY(e);
244
+ const i = rowAt(cur.y);
245
+ if (i >= 0 && i !== b.index) goTo(i, cur.y);
246
+ else wake();
247
+ };
248
+ const onLeave = (e) => {
249
+ if (e.pointerType === "touch" || b.index < 0) return;
250
+ const y = localY(e);
251
+ const row = rows[b.index];
252
+ release(y <= 0 ? "top" : y >= ul.clientHeight ? "bottom" : row && y < row.top + row.h / 2 ? "top" : "bottom");
253
+ };
254
+ const onDown = (e) => {
255
+ if (e.pointerType !== "touch") return;
256
+ const y = localY(e);
257
+ const i = rowAt(y);
258
+ if (i >= 0) goTo(i, y);
259
+ };
260
+ ul.addEventListener("pointermove", onMove);
261
+ ul.addEventListener("pointerleave", onLeave);
262
+ ul.addEventListener("pointerdown", onDown);
263
+ engine.current = { goTo, release };
264
+
265
+ const ro = new ResizeObserver(measure);
266
+ ro.observe(nav);
267
+ const io = new IntersectionObserver(([en]) => {
268
+ visible = en.isIntersecting;
269
+ if (visible) wake();
270
+ });
271
+ io.observe(nav);
272
+ measure();
273
+
274
+ return () => {
275
+ cancelAnimationFrame(raf);
276
+ ro.disconnect();
277
+ io.disconnect();
278
+ ul.removeEventListener("pointermove", onMove);
279
+ ul.removeEventListener("pointerleave", onLeave);
280
+ ul.removeEventListener("pointerdown", onDown);
281
+ engine.current = null;
282
+ };
283
+ }, [list_.length]);
284
+
285
+ // Rows rise in and the lines draw once on mount.
286
+ useEffect(() => {
287
+ const nav = root.current;
288
+ if (!intro || !nav || window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) return undefined;
289
+ const ease = "cubic-bezier(0.22, 1, 0.36, 1)";
290
+ const anims = [
291
+ ...[...nav.querySelectorAll("[data-rise]")].map((el, i) => el.animate([{ transform: "translateY(105%)" }, { transform: "none" }], { duration: 850, delay: 80 + i * 80, easing: ease, fill: "backwards" })),
292
+ ...[...nav.querySelectorAll("[data-rule]")].map((el, i) => el.animate([{ transform: "scaleX(0)" }, { transform: "none" }], { duration: 1000, delay: i * 80, easing: ease, fill: "backwards" })),
293
+ ];
294
+ return () => anims.forEach((a) => a.cancel());
295
+ }, [intro]);
296
+
297
+ const shape = SHAPES[imageShape] ?? SHAPES.pill;
298
+
299
+ return (
300
+ <nav ref={root} aria-label="Menu" className={`relative w-full select-none overflow-hidden ${className}`} style={{ height, background: menuBg, color: menuColor }}>
301
+ <ul ref={list} className="relative m-0 flex h-full list-none flex-col p-0">
302
+ {list_.map((item, i) => {
303
+ const Tag = item.href ? "a" : "button";
304
+ return (
305
+ <li key={i} ref={(e) => (rowEls.current[i] = e)} className="group relative min-h-[3.5rem] flex-1 overflow-hidden [container-type:size]">
306
+ {i > 0 && <span data-rule="" aria-hidden="true" className="pointer-events-none absolute inset-x-0 top-0 h-px origin-left" style={{ background: lineColor }} />}
307
+ <Tag
308
+ {...(item.href ? { href: item.href } : { type: "button" })}
309
+ onFocus={(e) => e.currentTarget.matches(":focus-visible") && engine.current?.goTo(i)}
310
+ onBlur={(e) => !list.current?.contains(e.relatedTarget) && engine.current?.release("bottom")}
311
+ onClick={(e) => live.current.onSelect?.(item, i, e)}
312
+ className={`relative flex h-full w-full cursor-pointer items-center justify-center border-0 bg-transparent ${showIndex ? "px-[clamp(2.4rem,10cqw,5rem)]" : "px-[clamp(1rem,4cqw,2.5rem)]"} text-inherit no-underline outline-none transition-opacity duration-500 group-data-[dim]:opacity-30 focus-visible:outline-2 focus-visible:-outline-offset-4 focus-visible:outline-current`}
313
+ >
314
+ {showIndex && <span className="absolute left-[clamp(1rem,4cqw,2.5rem)] font-mono text-[clamp(0.65rem,11cqh,0.9rem)] tabular-nums opacity-50">{String(i + 1).padStart(2, "0")}</span>}
315
+ <span className="block overflow-hidden py-[0.06em]">
316
+ <span data-rise="" className="block whitespace-nowrap text-[clamp(1rem,min(38cqh,7cqw),6.5rem)] font-semibold uppercase leading-[1.05] tracking-[-0.02em]">
317
+ {item.title}
318
+ </span>
319
+ </span>
320
+ </Tag>
321
+ </li>
322
+ );
323
+ })}
324
+ </ul>
325
+ <div ref={bandRef} aria-hidden="true" className="pointer-events-none absolute inset-0 z-10" style={{ background: bgColor, color: textColor, visibility: "hidden", clipPath: "inset(50% 0 50% 0)" }}>
326
+ {list_.map((item, i) => (
327
+ <div key={i} ref={(e) => (strips.current[i] = e)} className="absolute inset-x-0 overflow-hidden [container-type:size]">
328
+ <div className="flex h-full w-max items-center will-change-transform">
329
+ {Array.from({ length: copies[i] ?? 3 }, (_, k) => (
330
+ <Part key={k} item={item} shape={shape} />
331
+ ))}
332
+ </div>
333
+ </div>
334
+ ))}
335
+ </div>
336
+ </nav>
337
+ );
338
+ };
339
+
340
+ export default AccordionMarquee;
@@ -58,7 +58,7 @@ const AnimatedTabs = ({ tabs = DEFAULT_TABS, defaultTab, variant = "pill", accen
58
58
  aria-controls={`${id}-panel`}
59
59
  tabIndex={on ? 0 : -1}
60
60
  onClick={() => select(i)}
61
- className={`relative flex-1 whitespace-nowrap px-4 py-2 text-[0.88rem] font-medium outline-none transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-current/40 ${variant === "underline" ? "pb-3" : variant === "pill" ? "rounded-full" : "rounded-lg"} ${on ? (variant === "pill" ? "text-neutral-950" : "") : dark ? "text-white/55 hover:text-white" : "text-neutral-500 hover:text-neutral-900"}`}
61
+ className={`relative flex-1 whitespace-nowrap px-3 py-2 text-[0.82rem] sm:px-4 sm:text-[0.88rem] font-medium outline-none transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-current/40 ${variant === "underline" ? "pb-3" : variant === "pill" ? "rounded-full" : "rounded-lg"} ${on ? (variant === "pill" ? "text-neutral-950" : "") : dark ? "text-white/55 hover:text-white" : "text-neutral-500 hover:text-neutral-900"}`}
62
62
  >
63
63
  {on && (
64
64
  <motion.span