overlapping-cards-scroll 0.1.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.
package/dist/index.cjs ADDED
@@ -0,0 +1,716 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all)
7
+ __defProp(target, name, { get: all[name], enumerable: true });
8
+ };
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
+
19
+ // src/lib/index.ts
20
+ var index_exports = {};
21
+ __export(index_exports, {
22
+ OverlappingCardsScroll: () => OverlappingCardsScroll,
23
+ OverlappingCardsScrollFocusTrigger: () => OverlappingCardsScrollFocusTrigger
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // src/lib/OverlappingCardsScroll.tsx
28
+ var import_react = require("react");
29
+ var import_jsx_runtime = require("react/jsx-runtime");
30
+ var clamp = (value, min, max) => Math.min(Math.max(value, min), max);
31
+ var toCssDimension = (value) => typeof value === "number" ? `${value}px` : value;
32
+ var PAGE_DOT_POSITIONS = /* @__PURE__ */ new Set(["above", "below", "overlay"]);
33
+ var normalizePageDotsPosition = (value) => PAGE_DOT_POSITIONS.has(value) ? value : "below";
34
+ var TAB_POSITIONS = /* @__PURE__ */ new Set(["above", "below"]);
35
+ var normalizeTabsPosition = (value) => TAB_POSITIONS.has(value) ? value : "above";
36
+ function DefaultTabsContainerComponent({
37
+ children,
38
+ className,
39
+ style,
40
+ ariaLabel
41
+ }) {
42
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("nav", { className, style, "aria-label": ariaLabel, children });
43
+ }
44
+ function DefaultTabsComponent({
45
+ name,
46
+ className,
47
+ style,
48
+ ariaLabel,
49
+ ariaCurrent,
50
+ onClick
51
+ }) {
52
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
53
+ "button",
54
+ {
55
+ type: "button",
56
+ className,
57
+ "aria-label": ariaLabel,
58
+ "aria-current": ariaCurrent,
59
+ onClick,
60
+ style,
61
+ children: name
62
+ }
63
+ );
64
+ }
65
+ var resolveCardX = (index, principalIndex, transitionProgress, layout) => {
66
+ if (index <= principalIndex) {
67
+ return index * layout.peek;
68
+ }
69
+ let cardX = principalIndex * layout.peek + layout.cardWidth + (index - principalIndex - 1) * layout.peek;
70
+ if (index === principalIndex + 1) {
71
+ cardX -= transitionProgress * (layout.cardWidth - layout.peek);
72
+ }
73
+ return cardX;
74
+ };
75
+ var OverlappingCardsScrollControllerContext = (0, import_react.createContext)(null);
76
+ var OverlappingCardsScrollCardIndexContext = (0, import_react.createContext)(null);
77
+ function useOverlappingCardsScrollCardControl() {
78
+ const controller = (0, import_react.useContext)(OverlappingCardsScrollControllerContext);
79
+ const cardIndex = (0, import_react.useContext)(OverlappingCardsScrollCardIndexContext);
80
+ const canFocus = controller !== null && cardIndex !== null;
81
+ const focusCard = (0, import_react.useCallback)(
82
+ (options = {}) => {
83
+ if (!canFocus) {
84
+ return;
85
+ }
86
+ controller.focusCard(cardIndex, options);
87
+ },
88
+ [canFocus, cardIndex, controller]
89
+ );
90
+ return {
91
+ cardIndex,
92
+ canFocus,
93
+ focusCard
94
+ };
95
+ }
96
+ function OverlappingCardsScrollFocusTrigger({
97
+ children = "Make principal",
98
+ className = "",
99
+ behavior = "smooth",
100
+ transitionMode = "swoop",
101
+ onClick = void 0,
102
+ ...buttonProps
103
+ }) {
104
+ const { canFocus, focusCard } = useOverlappingCardsScrollCardControl();
105
+ const handleClick = (event) => {
106
+ onClick == null ? void 0 : onClick(event);
107
+ if (!event.defaultPrevented) {
108
+ focusCard({ behavior, transitionMode });
109
+ }
110
+ };
111
+ const buttonClassName = className ? `ocs-focus-trigger ${className}` : "ocs-focus-trigger";
112
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: buttonClassName, disabled: !canFocus, onClick: handleClick, ...buttonProps, children });
113
+ }
114
+ var resolveCardWidth = (cardWidth, viewportWidth, fallbackRatio) => {
115
+ if (typeof cardWidth === "number" && Number.isFinite(cardWidth) && cardWidth > 0) {
116
+ return cardWidth;
117
+ }
118
+ if (typeof cardWidth === "string") {
119
+ const value = cardWidth.trim();
120
+ if (value.endsWith("%")) {
121
+ const percent = Number.parseFloat(value.slice(0, -1));
122
+ if (Number.isFinite(percent) && percent > 0) {
123
+ return viewportWidth * percent / 100;
124
+ }
125
+ }
126
+ const numeric = Number.parseFloat(value);
127
+ if (Number.isFinite(numeric) && numeric > 0) {
128
+ return numeric;
129
+ }
130
+ }
131
+ return viewportWidth * fallbackRatio;
132
+ };
133
+ function OverlappingCardsScroll(props) {
134
+ const {
135
+ className = "",
136
+ cardHeight = 300,
137
+ cardWidth = void 0,
138
+ cardWidthRatio = 1 / 3,
139
+ basePeek = 64,
140
+ minPeek = 10,
141
+ maxPeek = 84,
142
+ showPageDots = false,
143
+ pageDotsPosition = "below",
144
+ pageDotsOffset = 10,
145
+ pageDotsBehavior = "smooth",
146
+ pageDotsClassName = "",
147
+ cardContainerClassName = "",
148
+ cardContainerStyle = {},
149
+ snapToCardOnRelease = true,
150
+ snapReleaseDelay = 800,
151
+ focusTransitionDuration = 420,
152
+ ariaLabel = "Overlapping cards scroll",
153
+ showTabs = false,
154
+ tabsPosition = "above",
155
+ tabsOffset = 10,
156
+ tabsBehavior = "smooth",
157
+ tabsClassName = "",
158
+ tabsComponent: TabsComponent = DefaultTabsComponent,
159
+ tabsContainerComponent: TabsContainerComponent = DefaultTabsContainerComponent
160
+ } = props;
161
+ const hasItems = "items" in props && Array.isArray(props.items);
162
+ const hasChildren = "children" in props && props.children != null;
163
+ (0, import_react.useEffect)(() => {
164
+ if (hasItems && hasChildren) {
165
+ console.warn(
166
+ "OverlappingCardsScroll: Both `items` and `children` were provided. `items` takes precedence."
167
+ );
168
+ }
169
+ }, [hasItems, hasChildren]);
170
+ const itemsProp = hasItems ? props.items : null;
171
+ const childrenProp = hasChildren ? props.children : null;
172
+ const cards = (0, import_react.useMemo)(() => {
173
+ if (itemsProp) {
174
+ return itemsProp.map((item) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react.Fragment, { children: item.jsx }, item.id));
175
+ }
176
+ return import_react.Children.toArray(childrenProp);
177
+ }, [itemsProp, childrenProp]);
178
+ const cardNames = (0, import_react.useMemo)(() => {
179
+ if (itemsProp) {
180
+ return itemsProp.map((item) => item.name);
181
+ }
182
+ return null;
183
+ }, [itemsProp]);
184
+ const cardCount = cards.length;
185
+ const containerRef = (0, import_react.useRef)(null);
186
+ const scrollRef = (0, import_react.useRef)(null);
187
+ const touchStateRef = (0, import_react.useRef)(null);
188
+ const snapTimeoutRef = (0, import_react.useRef)(null);
189
+ const shouldSnapOnMouseMoveRef = (0, import_react.useRef)(false);
190
+ const focusTransitionTimeoutRef = (0, import_react.useRef)(null);
191
+ const [viewportWidth, setViewportWidth] = (0, import_react.useState)(1);
192
+ const [scrollLeft, setScrollLeft] = (0, import_react.useState)(0);
193
+ const [focusTransition, setFocusTransition] = (0, import_react.useState)(null);
194
+ const clearSnapTimeout = (0, import_react.useCallback)(() => {
195
+ if (snapTimeoutRef.current !== null) {
196
+ clearTimeout(snapTimeoutRef.current);
197
+ snapTimeoutRef.current = null;
198
+ }
199
+ }, []);
200
+ const clearFocusTransitionTimeout = (0, import_react.useCallback)(() => {
201
+ if (focusTransitionTimeoutRef.current !== null) {
202
+ clearTimeout(focusTransitionTimeoutRef.current);
203
+ focusTransitionTimeoutRef.current = null;
204
+ }
205
+ }, []);
206
+ const cancelFocusTransition = (0, import_react.useCallback)(() => {
207
+ clearFocusTransitionTimeout();
208
+ setFocusTransition(null);
209
+ }, [clearFocusTransitionTimeout]);
210
+ (0, import_react.useEffect)(() => {
211
+ const containerElement = containerRef.current;
212
+ const scrollElement = scrollRef.current;
213
+ if (!containerElement || !scrollElement) {
214
+ return void 0;
215
+ }
216
+ const syncScroll = () => {
217
+ setScrollLeft(scrollElement.scrollLeft);
218
+ };
219
+ const resizeObserver = new ResizeObserver((entries) => {
220
+ var _a, _b;
221
+ const entry = entries[0];
222
+ const width = (_b = (_a = entry == null ? void 0 : entry.contentRect) == null ? void 0 : _a.width) != null ? _b : 1;
223
+ setViewportWidth(Math.max(1, width));
224
+ syncScroll();
225
+ });
226
+ resizeObserver.observe(containerElement);
227
+ setViewportWidth(Math.max(1, containerElement.getBoundingClientRect().width || 1));
228
+ syncScroll();
229
+ scrollElement.addEventListener("scroll", syncScroll, { passive: true });
230
+ return () => {
231
+ resizeObserver.disconnect();
232
+ scrollElement.removeEventListener("scroll", syncScroll);
233
+ };
234
+ }, []);
235
+ (0, import_react.useEffect)(() => () => clearSnapTimeout(), [clearSnapTimeout]);
236
+ (0, import_react.useEffect)(() => () => clearFocusTransitionTimeout(), [clearFocusTransitionTimeout]);
237
+ (0, import_react.useEffect)(() => {
238
+ if (snapToCardOnRelease && cardCount > 1) {
239
+ return;
240
+ }
241
+ clearSnapTimeout();
242
+ shouldSnapOnMouseMoveRef.current = false;
243
+ cancelFocusTransition();
244
+ }, [cancelFocusTransition, cardCount, clearSnapTimeout, snapToCardOnRelease]);
245
+ (0, import_react.useEffect)(() => {
246
+ if (cardCount > 1) {
247
+ return;
248
+ }
249
+ cancelFocusTransition();
250
+ }, [cancelFocusTransition, cardCount]);
251
+ const layout = (0, import_react.useMemo)(() => {
252
+ const safeWidth = Math.max(1, viewportWidth);
253
+ const safeRatio = clamp(cardWidthRatio, 0.2, 0.95);
254
+ const width = Math.max(1, resolveCardWidth(cardWidth, safeWidth, safeRatio));
255
+ if (cardCount < 2) {
256
+ return {
257
+ cardWidth: width,
258
+ peek: 0,
259
+ stepDistance: 1,
260
+ scrollRange: 0,
261
+ trackWidth: safeWidth
262
+ };
263
+ }
264
+ const availableStackWidth = Math.max(0, safeWidth - width);
265
+ const maxVisiblePeek = availableStackWidth / (cardCount - 1);
266
+ const preferredPeek = clamp(basePeek, minPeek, maxPeek);
267
+ const peek = Math.min(preferredPeek, maxVisiblePeek);
268
+ const stepDistance = Math.max(1, width - peek);
269
+ const scrollRange = stepDistance * (cardCount - 1);
270
+ const trackWidth = safeWidth + scrollRange;
271
+ return {
272
+ cardWidth: width,
273
+ peek,
274
+ stepDistance,
275
+ scrollRange,
276
+ trackWidth
277
+ };
278
+ }, [basePeek, cardCount, cardWidth, cardWidthRatio, maxPeek, minPeek, viewportWidth]);
279
+ (0, import_react.useEffect)(() => {
280
+ const scrollElement = scrollRef.current;
281
+ if (!scrollElement) {
282
+ return;
283
+ }
284
+ if (scrollElement.scrollLeft > layout.scrollRange) {
285
+ scrollElement.scrollLeft = layout.scrollRange;
286
+ setScrollLeft(layout.scrollRange);
287
+ }
288
+ }, [layout.scrollRange]);
289
+ const progress = cardCount > 1 ? clamp(scrollLeft / layout.stepDistance, 0, cardCount - 1) : 0;
290
+ const activeIndex = Math.floor(progress);
291
+ const transitionProgress = progress - activeIndex;
292
+ const snapToNearestCard = (0, import_react.useCallback)(
293
+ (options = {}) => {
294
+ var _a;
295
+ if (!snapToCardOnRelease || cardCount < 2) {
296
+ return;
297
+ }
298
+ const scrollElement = scrollRef.current;
299
+ if (!scrollElement) {
300
+ return;
301
+ }
302
+ const currentScrollLeft = clamp(scrollElement.scrollLeft, 0, layout.scrollRange);
303
+ const nearestIndex = clamp(
304
+ Math.round(currentScrollLeft / layout.stepDistance),
305
+ 0,
306
+ cardCount - 1
307
+ );
308
+ const targetScrollLeft = clamp(
309
+ nearestIndex * layout.stepDistance,
310
+ 0,
311
+ layout.scrollRange
312
+ );
313
+ if (Math.abs(targetScrollLeft - currentScrollLeft) < 1) {
314
+ return;
315
+ }
316
+ const behavior = (_a = options.behavior) != null ? _a : "smooth";
317
+ if (typeof scrollElement.scrollTo === "function") {
318
+ scrollElement.scrollTo({
319
+ left: targetScrollLeft,
320
+ behavior
321
+ });
322
+ } else {
323
+ scrollElement.scrollLeft = targetScrollLeft;
324
+ }
325
+ if (behavior === "auto") {
326
+ setScrollLeft(targetScrollLeft);
327
+ }
328
+ },
329
+ [cardCount, layout.scrollRange, layout.stepDistance, snapToCardOnRelease]
330
+ );
331
+ const scheduleSnapToNearestCard = (0, import_react.useCallback)(
332
+ (delay = snapReleaseDelay) => {
333
+ if (!snapToCardOnRelease || cardCount < 2) {
334
+ return;
335
+ }
336
+ const safeDelay = Number.isFinite(delay) ? Math.max(0, delay) : 800;
337
+ clearSnapTimeout();
338
+ snapTimeoutRef.current = setTimeout(() => {
339
+ snapTimeoutRef.current = null;
340
+ shouldSnapOnMouseMoveRef.current = false;
341
+ snapToNearestCard({ behavior: "smooth" });
342
+ }, safeDelay);
343
+ },
344
+ [cardCount, clearSnapTimeout, snapReleaseDelay, snapToCardOnRelease, snapToNearestCard]
345
+ );
346
+ const markSnapCandidateFromScroll = (0, import_react.useCallback)(() => {
347
+ if (!snapToCardOnRelease || cardCount < 2) {
348
+ return;
349
+ }
350
+ shouldSnapOnMouseMoveRef.current = true;
351
+ scheduleSnapToNearestCard();
352
+ }, [cardCount, scheduleSnapToNearestCard, snapToCardOnRelease]);
353
+ (0, import_react.useEffect)(() => {
354
+ if (typeof window === "undefined" || !snapToCardOnRelease || cardCount < 2) {
355
+ return void 0;
356
+ }
357
+ const handleMouseMove = () => {
358
+ if (!shouldSnapOnMouseMoveRef.current) {
359
+ return;
360
+ }
361
+ shouldSnapOnMouseMoveRef.current = false;
362
+ clearSnapTimeout();
363
+ snapToNearestCard({ behavior: "smooth" });
364
+ };
365
+ window.addEventListener("mousemove", handleMouseMove, { passive: true });
366
+ return () => {
367
+ window.removeEventListener("mousemove", handleMouseMove);
368
+ };
369
+ }, [cardCount, clearSnapTimeout, snapToCardOnRelease, snapToNearestCard]);
370
+ const focusCard = (0, import_react.useCallback)(
371
+ (targetIndex, options = {}) => {
372
+ var _a, _b, _c;
373
+ const scrollElement = scrollRef.current;
374
+ if (!scrollElement || cardCount === 0) {
375
+ return;
376
+ }
377
+ clearSnapTimeout();
378
+ shouldSnapOnMouseMoveRef.current = false;
379
+ cancelFocusTransition();
380
+ const safeIndex = clamp(Math.round(targetIndex), 0, cardCount - 1);
381
+ const nextScrollLeft = clamp(safeIndex * layout.stepDistance, 0, layout.scrollRange);
382
+ const transitionMode = (_a = options.transitionMode) != null ? _a : "swoop";
383
+ if (transitionMode === "swoop") {
384
+ const duration = Number.isFinite(options.duration) ? Math.max(0, options.duration) : focusTransitionDuration;
385
+ clearFocusTransitionTimeout();
386
+ setFocusTransition({ duration });
387
+ scrollElement.scrollLeft = nextScrollLeft;
388
+ setScrollLeft(nextScrollLeft);
389
+ if (duration <= 0) {
390
+ setFocusTransition(null);
391
+ return;
392
+ }
393
+ focusTransitionTimeoutRef.current = setTimeout(() => {
394
+ focusTransitionTimeoutRef.current = null;
395
+ setFocusTransition(null);
396
+ }, duration + 40);
397
+ return;
398
+ }
399
+ if (typeof scrollElement.scrollTo === "function") {
400
+ scrollElement.scrollTo({
401
+ left: nextScrollLeft,
402
+ behavior: (_b = options.behavior) != null ? _b : "smooth"
403
+ });
404
+ } else {
405
+ scrollElement.scrollLeft = nextScrollLeft;
406
+ }
407
+ if (((_c = options.behavior) != null ? _c : "smooth") === "auto") {
408
+ setScrollLeft(nextScrollLeft);
409
+ }
410
+ },
411
+ [
412
+ cardCount,
413
+ cancelFocusTransition,
414
+ clearFocusTransitionTimeout,
415
+ clearSnapTimeout,
416
+ focusTransitionDuration,
417
+ layout.scrollRange,
418
+ layout.stepDistance
419
+ ]
420
+ );
421
+ const controllerContextValue = (0, import_react.useMemo)(
422
+ () => ({
423
+ focusCard
424
+ }),
425
+ [focusCard]
426
+ );
427
+ const setControllerScroll = (0, import_react.useCallback)(
428
+ (nextValue) => {
429
+ const scrollElement = scrollRef.current;
430
+ if (!scrollElement) {
431
+ return;
432
+ }
433
+ const nextScrollLeft = clamp(nextValue, 0, layout.scrollRange);
434
+ if (scrollElement.scrollLeft !== nextScrollLeft) {
435
+ scrollElement.scrollLeft = nextScrollLeft;
436
+ }
437
+ setScrollLeft(nextScrollLeft);
438
+ },
439
+ [layout.scrollRange]
440
+ );
441
+ const applyScrollDelta = (0, import_react.useCallback)(
442
+ (delta) => {
443
+ const scrollElement = scrollRef.current;
444
+ if (!scrollElement) {
445
+ return;
446
+ }
447
+ setControllerScroll(scrollElement.scrollLeft + delta);
448
+ },
449
+ [setControllerScroll]
450
+ );
451
+ const handleWheel = (0, import_react.useCallback)(
452
+ (event) => {
453
+ if (cardCount < 2) {
454
+ return;
455
+ }
456
+ const absX = Math.abs(event.deltaX);
457
+ const absY = Math.abs(event.deltaY);
458
+ if (absX === 0 && absY === 0) {
459
+ return;
460
+ }
461
+ if (absY > absX) {
462
+ return;
463
+ }
464
+ event.preventDefault();
465
+ cancelFocusTransition();
466
+ applyScrollDelta(event.deltaX);
467
+ markSnapCandidateFromScroll();
468
+ },
469
+ [cardCount, cancelFocusTransition, applyScrollDelta, markSnapCandidateFromScroll]
470
+ );
471
+ const handleTouchStart = (event) => {
472
+ if (cardCount < 2) {
473
+ return;
474
+ }
475
+ const scrollElement = scrollRef.current;
476
+ const touch = event.touches[0];
477
+ if (!scrollElement || !touch) {
478
+ return;
479
+ }
480
+ cancelFocusTransition();
481
+ touchStateRef.current = {
482
+ startX: touch.clientX,
483
+ startScrollLeft: scrollElement.scrollLeft
484
+ };
485
+ };
486
+ const handleTouchMove = (event) => {
487
+ const touchState = touchStateRef.current;
488
+ const touch = event.touches[0];
489
+ if (!touchState || !touch) {
490
+ return;
491
+ }
492
+ const delta = touchState.startX - touch.clientX;
493
+ if (Math.abs(delta) < 2) {
494
+ return;
495
+ }
496
+ event.preventDefault();
497
+ setControllerScroll(touchState.startScrollLeft + delta);
498
+ markSnapCandidateFromScroll();
499
+ };
500
+ const handleTouchEnd = () => {
501
+ if (touchStateRef.current && snapToCardOnRelease && cardCount > 1) {
502
+ scheduleSnapToNearestCard(80);
503
+ }
504
+ touchStateRef.current = null;
505
+ };
506
+ const stageRef = (0, import_react.useRef)(null);
507
+ (0, import_react.useEffect)(() => {
508
+ const stageElement = stageRef.current;
509
+ if (!stageElement) {
510
+ return void 0;
511
+ }
512
+ stageElement.addEventListener("wheel", handleWheel, { passive: false });
513
+ return () => {
514
+ stageElement.removeEventListener("wheel", handleWheel);
515
+ };
516
+ }, [handleWheel]);
517
+ const containerClassName = className ? `overlapping-cards-scroll ${className}` : "overlapping-cards-scroll";
518
+ const resolvedPageDotsPosition = normalizePageDotsPosition(pageDotsPosition);
519
+ const showNavigationDots = showPageDots && cardCount > 1;
520
+ const resolvedTabsPosition = normalizeTabsPosition(tabsPosition);
521
+ const showNavigationTabs = showTabs && cardCount > 1 && cardNames !== null;
522
+ (0, import_react.useEffect)(() => {
523
+ if (showTabs && cardNames === null) {
524
+ console.warn(
525
+ "OverlappingCardsScroll: `showTabs` requires the `items` prop to provide card names. Tabs will not render."
526
+ );
527
+ }
528
+ }, [showTabs, cardNames]);
529
+ const renderTabs = (position) => {
530
+ if (!showNavigationTabs || cardNames === null) {
531
+ return null;
532
+ }
533
+ const containerClassName2 = tabsClassName ? `ocs-tabs ocs-tabs--${position} ${tabsClassName}` : `ocs-tabs ocs-tabs--${position}`;
534
+ const containerStyle = position === "above" ? { marginBottom: toCssDimension(tabsOffset) } : { marginTop: toCssDimension(tabsOffset) };
535
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
536
+ TabsContainerComponent,
537
+ {
538
+ position,
539
+ className: containerClassName2,
540
+ style: containerStyle,
541
+ ariaLabel: "Card tabs",
542
+ cardNames,
543
+ activeIndex,
544
+ progress,
545
+ children: cardNames.map((name, index) => {
546
+ const influence = clamp(1 - Math.abs(progress - index), 0, 1);
547
+ const isPrincipal = influence > 0.98;
548
+ const animate = {
549
+ opacity: 0.45 + influence * 0.55
550
+ };
551
+ const className2 = isPrincipal ? "ocs-tab ocs-tab--active" : "ocs-tab";
552
+ const style = { opacity: animate.opacity };
553
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
554
+ TabsComponent,
555
+ {
556
+ name,
557
+ index,
558
+ position,
559
+ isPrincipal,
560
+ influence,
561
+ animate,
562
+ className: className2,
563
+ style,
564
+ ariaLabel: `Go to ${name}`,
565
+ ariaCurrent: isPrincipal ? "page" : void 0,
566
+ onClick: () => focusCard(index, {
567
+ behavior: tabsBehavior,
568
+ transitionMode: "swoop"
569
+ })
570
+ },
571
+ `ocs-tab-${position}-${index}`
572
+ );
573
+ })
574
+ }
575
+ );
576
+ };
577
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(OverlappingCardsScrollControllerContext.Provider, { value: controllerContextValue, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: containerClassName, "aria-label": ariaLabel, ref: containerRef, children: [
578
+ resolvedTabsPosition === "above" ? renderTabs("above") : null,
579
+ showNavigationDots && resolvedPageDotsPosition === "above" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
580
+ "nav",
581
+ {
582
+ className: pageDotsClassName ? `ocs-page-dots ocs-page-dots--above ${pageDotsClassName}` : "ocs-page-dots ocs-page-dots--above",
583
+ style: { marginBottom: toCssDimension(pageDotsOffset) },
584
+ "aria-label": "Card pages",
585
+ children: cards.map((_, index) => {
586
+ const influence = clamp(1 - Math.abs(progress - index), 0, 1);
587
+ const opacity = 0.25 + influence * 0.75;
588
+ const scale = 0.9 + influence * 0.22;
589
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
590
+ "button",
591
+ {
592
+ type: "button",
593
+ className: "ocs-page-dot",
594
+ "aria-label": `Go to card ${index + 1}`,
595
+ "aria-current": influence > 0.98 ? "page" : void 0,
596
+ onClick: () => focusCard(index, {
597
+ behavior: pageDotsBehavior,
598
+ transitionMode: "swoop"
599
+ }),
600
+ style: { opacity, transform: `scale(${scale})` }
601
+ },
602
+ `ocs-page-dot-above-${index}`
603
+ );
604
+ })
605
+ }
606
+ ) : null,
607
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ocs-stage-frame", children: [
608
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
609
+ "div",
610
+ {
611
+ className: "ocs-stage",
612
+ ref: stageRef,
613
+ style: {
614
+ minHeight: toCssDimension(cardHeight)
615
+ },
616
+ onTouchStart: handleTouchStart,
617
+ onTouchMove: handleTouchMove,
618
+ onTouchEnd: handleTouchEnd,
619
+ onTouchCancel: handleTouchEnd,
620
+ children: [
621
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
622
+ "div",
623
+ {
624
+ className: "ocs-track",
625
+ children: cards.map((card, index) => {
626
+ var _a;
627
+ const cardX = resolveCardX(index, activeIndex, transitionProgress, layout);
628
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
629
+ "div",
630
+ {
631
+ className: cardContainerClassName ? `${focusTransition ? "ocs-card ocs-card--focus-transition" : "ocs-card"} ${cardContainerClassName}` : focusTransition ? "ocs-card ocs-card--focus-transition" : "ocs-card",
632
+ style: {
633
+ width: `${layout.cardWidth}px`,
634
+ transform: `translate3d(${cardX}px, 0, 0)`,
635
+ transitionDuration: focusTransition ? `${focusTransition.duration}ms` : void 0,
636
+ ...cardContainerStyle
637
+ },
638
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(OverlappingCardsScrollCardIndexContext.Provider, { value: index, children: card })
639
+ },
640
+ (_a = card.key) != null ? _a : `ocs-card-${index}`
641
+ );
642
+ })
643
+ }
644
+ ),
645
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "ocs-scroll-region", ref: scrollRef, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
646
+ "div",
647
+ {
648
+ className: "ocs-scroll-spacer",
649
+ style: {
650
+ width: `${layout.trackWidth}px`
651
+ }
652
+ }
653
+ ) })
654
+ ]
655
+ }
656
+ ),
657
+ showNavigationDots && resolvedPageDotsPosition === "overlay" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
658
+ "nav",
659
+ {
660
+ className: pageDotsClassName ? `ocs-page-dots ocs-page-dots--overlay ${pageDotsClassName}` : "ocs-page-dots ocs-page-dots--overlay",
661
+ style: { bottom: toCssDimension(pageDotsOffset) },
662
+ "aria-label": "Card pages",
663
+ children: cards.map((_, index) => {
664
+ const influence = clamp(1 - Math.abs(progress - index), 0, 1);
665
+ const opacity = 0.25 + influence * 0.75;
666
+ const scale = 0.9 + influence * 0.22;
667
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
668
+ "button",
669
+ {
670
+ type: "button",
671
+ className: "ocs-page-dot",
672
+ "aria-label": `Go to card ${index + 1}`,
673
+ "aria-current": influence > 0.98 ? "page" : void 0,
674
+ onClick: () => focusCard(index, {
675
+ behavior: pageDotsBehavior,
676
+ transitionMode: "swoop"
677
+ }),
678
+ style: { opacity, transform: `scale(${scale})` }
679
+ },
680
+ `ocs-page-dot-overlay-${index}`
681
+ );
682
+ })
683
+ }
684
+ ) : null
685
+ ] }),
686
+ showNavigationDots && resolvedPageDotsPosition === "below" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
687
+ "nav",
688
+ {
689
+ className: pageDotsClassName ? `ocs-page-dots ocs-page-dots--below ${pageDotsClassName}` : "ocs-page-dots ocs-page-dots--below",
690
+ style: { marginTop: toCssDimension(pageDotsOffset) },
691
+ "aria-label": "Card pages",
692
+ children: cards.map((_, index) => {
693
+ const influence = clamp(1 - Math.abs(progress - index), 0, 1);
694
+ const opacity = 0.25 + influence * 0.75;
695
+ const scale = 0.9 + influence * 0.22;
696
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
697
+ "button",
698
+ {
699
+ type: "button",
700
+ className: "ocs-page-dot",
701
+ "aria-label": `Go to card ${index + 1}`,
702
+ "aria-current": influence > 0.98 ? "page" : void 0,
703
+ onClick: () => focusCard(index, {
704
+ behavior: pageDotsBehavior,
705
+ transitionMode: "swoop"
706
+ }),
707
+ style: { opacity, transform: `scale(${scale})` }
708
+ },
709
+ `ocs-page-dot-below-${index}`
710
+ );
711
+ })
712
+ }
713
+ ) : null,
714
+ resolvedTabsPosition === "below" ? renderTabs("below") : null
715
+ ] }) });
716
+ }