overlapping-cards-scroll 0.1.0 → 0.1.2

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/README.md CHANGED
@@ -68,16 +68,24 @@ import 'overlapping-cards-scroll/styles.css'
68
68
  ## Props
69
69
 
70
70
  - `children`: card nodes
71
+ - `items` (`{ id: string | number; name: string; jsx: ReactElement }[]`) optional alternative to `children`; required for named tabs (`showTabs`)
71
72
  - `cardHeight` (`number | string`, default `300`)
72
73
  - `cardWidth` (`number | string`) accepts px number (e.g. `250`) or percent string (e.g. `'50%'`)
73
74
  - `cardWidthRatio` (`number`, default `1 / 3`)
74
75
  - `basePeek` (`number`, default `64`)
75
76
  - `minPeek` (`number`, default `10`)
76
77
  - `maxPeek` (`number`, default `84`)
78
+ - `cardContainerStyle` (web: `CSSProperties`, RN native: `StyleProp<ViewStyle>`) applied to each positioned card container
77
79
  - `showPageDots` (`boolean`, default `false`) renders clickable page dots
78
80
  - `pageDotsPosition` (`'above' | 'below' | 'overlay'`, default `'below'`)
79
81
  - `pageDotsOffset` (`number | string`, default `10`) distance from stage
80
82
  - `pageDotsBehavior` (`'smooth' | 'auto'`, web only, default `'smooth'`)
83
+ - `showTabs` (`boolean`, default `false`) renders card-name tabs when `items` is provided
84
+ - `tabsPosition` (`'above' | 'below'`, default `'above'`)
85
+ - `tabsOffset` (`number | string`, default `10`)
86
+ - `tabsBehavior` (`'smooth' | 'auto'`, web only, default `'smooth'`)
87
+ - `tabsComponent` (custom tab renderer; supported on web and RN native)
88
+ - `tabsContainerComponent` (custom tab container renderer; supported on web and RN native)
81
89
  - `snapToCardOnRelease` (`boolean`, default `true`) web/RN-web uses idle + mousemove snap, RN native uses `snapToInterval` on iOS
82
90
  - `snapReleaseDelay` (`number`, default `800`) web/RN-web debounce before auto-snap
83
91
  - `snapDecelerationRate` (`'normal' | 'fast' | number`, RN native only, default `'normal'`) controls fling feel while snapping
@@ -106,6 +114,14 @@ Expo development:
106
114
  npm run dev:expo
107
115
  ```
108
116
 
117
+ The Expo app shell lives in `expo-demo/` and imports the demo screen from the package root via `../App`.
118
+ You can also run it directly with:
119
+
120
+ ```bash
121
+ cd expo-demo
122
+ npm run dev
123
+ ```
124
+
109
125
  Build package artifacts:
110
126
 
111
127
  ```bash
@@ -23,7 +23,7 @@ __export(OverlappingCardsScrollRN_web_exports, {
23
23
  OverlappingCardsScrollRNFocusTrigger: () => OverlappingCardsScrollRNFocusTrigger
24
24
  });
25
25
  module.exports = __toCommonJS(OverlappingCardsScrollRN_web_exports);
26
- var import_react_native_web = require("react-native-web");
26
+ var import_react_native = require("react-native");
27
27
 
28
28
  // src/lib/OverlappingCardsScroll.tsx
29
29
  var import_react = require("react");
@@ -721,10 +721,19 @@ var import_jsx_runtime2 = require("react/jsx-runtime");
721
721
  function OverlappingCardsScrollRNFocusTrigger({
722
722
  children = "Make principal",
723
723
  className = "",
724
+ style = void 0,
725
+ textStyle = void 0,
726
+ behavior = "smooth",
727
+ transitionMode = "swoop",
728
+ disabled = false,
729
+ accessibilityLabel = void 0,
730
+ testID = void 0,
724
731
  onPress = void 0,
725
732
  onClick = void 0,
726
733
  ...buttonProps
727
734
  }) {
735
+ void style;
736
+ void textStyle;
728
737
  const handleClick = (event) => {
729
738
  onClick == null ? void 0 : onClick(event);
730
739
  onPress == null ? void 0 : onPress(event);
@@ -733,6 +742,11 @@ function OverlappingCardsScrollRNFocusTrigger({
733
742
  OverlappingCardsScrollFocusTrigger,
734
743
  {
735
744
  className,
745
+ behavior,
746
+ transitionMode,
747
+ disabled,
748
+ "aria-label": accessibilityLabel,
749
+ "data-testid": testID,
736
750
  onClick: handleClick,
737
751
  ...buttonProps,
738
752
  children
@@ -740,7 +754,6 @@ function OverlappingCardsScrollRNFocusTrigger({
740
754
  );
741
755
  }
742
756
  function OverlappingCardsScrollRN({
743
- children,
744
757
  style = void 0,
745
758
  showsHorizontalScrollIndicator = true,
746
759
  snapDecelerationRate = "normal",
@@ -750,9 +763,14 @@ function OverlappingCardsScrollRN({
750
763
  void showsHorizontalScrollIndicator;
751
764
  void snapDecelerationRate;
752
765
  void snapDisableIntervalMomentum;
753
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native_web.View, { style: [styles.root, style], children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(OverlappingCardsScroll, { ...overlappingCardsScrollProps, children }) });
766
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_react_native.View, { style: [styles.root, style], children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
767
+ OverlappingCardsScroll,
768
+ {
769
+ ...overlappingCardsScrollProps
770
+ }
771
+ ) });
754
772
  }
755
- var styles = import_react_native_web.StyleSheet.create({
773
+ var styles = import_react_native.StyleSheet.create({
756
774
  root: {
757
775
  width: "100%",
758
776
  minWidth: 0,
@@ -1,5 +1,5 @@
1
1
  // src/rn/OverlappingCardsScrollRN.web.tsx
2
- import { StyleSheet, View } from "react-native-web";
2
+ import { StyleSheet, View } from "react-native";
3
3
 
4
4
  // src/lib/OverlappingCardsScroll.tsx
5
5
  import {
@@ -707,10 +707,19 @@ import { jsx as jsx2 } from "react/jsx-runtime";
707
707
  function OverlappingCardsScrollRNFocusTrigger({
708
708
  children = "Make principal",
709
709
  className = "",
710
+ style = void 0,
711
+ textStyle = void 0,
712
+ behavior = "smooth",
713
+ transitionMode = "swoop",
714
+ disabled = false,
715
+ accessibilityLabel = void 0,
716
+ testID = void 0,
710
717
  onPress = void 0,
711
718
  onClick = void 0,
712
719
  ...buttonProps
713
720
  }) {
721
+ void style;
722
+ void textStyle;
714
723
  const handleClick = (event) => {
715
724
  onClick == null ? void 0 : onClick(event);
716
725
  onPress == null ? void 0 : onPress(event);
@@ -719,6 +728,11 @@ function OverlappingCardsScrollRNFocusTrigger({
719
728
  OverlappingCardsScrollFocusTrigger,
720
729
  {
721
730
  className,
731
+ behavior,
732
+ transitionMode,
733
+ disabled,
734
+ "aria-label": accessibilityLabel,
735
+ "data-testid": testID,
722
736
  onClick: handleClick,
723
737
  ...buttonProps,
724
738
  children
@@ -726,7 +740,6 @@ function OverlappingCardsScrollRNFocusTrigger({
726
740
  );
727
741
  }
728
742
  function OverlappingCardsScrollRN({
729
- children,
730
743
  style = void 0,
731
744
  showsHorizontalScrollIndicator = true,
732
745
  snapDecelerationRate = "normal",
@@ -736,7 +749,12 @@ function OverlappingCardsScrollRN({
736
749
  void showsHorizontalScrollIndicator;
737
750
  void snapDecelerationRate;
738
751
  void snapDisableIntervalMomentum;
739
- return /* @__PURE__ */ jsx2(View, { style: [styles.root, style], children: /* @__PURE__ */ jsx2(OverlappingCardsScroll, { ...overlappingCardsScrollProps, children }) });
752
+ return /* @__PURE__ */ jsx2(View, { style: [styles.root, style], children: /* @__PURE__ */ jsx2(
753
+ OverlappingCardsScroll,
754
+ {
755
+ ...overlappingCardsScrollProps
756
+ }
757
+ ) });
740
758
  }
741
759
  var styles = StyleSheet.create({
742
760
  root: {
@@ -28,7 +28,43 @@ var import_react_native = require("react-native");
28
28
  var import_jsx_runtime = require("react/jsx-runtime");
29
29
  var clamp = (value, min, max) => Math.min(Math.max(value, min), max);
30
30
  var PAGE_DOT_POSITIONS = /* @__PURE__ */ new Set(["above", "below", "overlay"]);
31
+ var TAB_POSITIONS = /* @__PURE__ */ new Set(["above", "below"]);
31
32
  var normalizePageDotsPosition = (value) => PAGE_DOT_POSITIONS.has(value) ? value : "below";
33
+ var normalizeTabsPosition = (value) => TAB_POSITIONS.has(value) ? value : "above";
34
+ var toNumericOffset = (value, fallback = 0) => {
35
+ if (typeof value === "number" && Number.isFinite(value)) {
36
+ return value;
37
+ }
38
+ if (typeof value === "string") {
39
+ const parsed = Number.parseFloat(value.trim());
40
+ if (Number.isFinite(parsed)) {
41
+ return parsed;
42
+ }
43
+ }
44
+ return fallback;
45
+ };
46
+ var toNativeDimension = (value, fallback = 0) => {
47
+ if (typeof value === "number" && Number.isFinite(value)) {
48
+ return value;
49
+ }
50
+ if (typeof value === "string") {
51
+ const trimmed = value.trim();
52
+ if (trimmed === "auto") {
53
+ return "auto";
54
+ }
55
+ if (trimmed.endsWith("%")) {
56
+ const percent = Number.parseFloat(trimmed.slice(0, -1));
57
+ if (Number.isFinite(percent)) {
58
+ return `${percent}%`;
59
+ }
60
+ }
61
+ const numeric = Number.parseFloat(trimmed);
62
+ if (Number.isFinite(numeric)) {
63
+ return numeric;
64
+ }
65
+ }
66
+ return fallback;
67
+ };
32
68
  var resolveCardXAtProgress = (index, progress, layout) => {
33
69
  const principalIndex = Math.floor(progress);
34
70
  const transitionProgress = progress - principalIndex;
@@ -66,26 +102,69 @@ function OverlappingCardsScrollRNFocusTrigger({
66
102
  children = "Make principal",
67
103
  style = void 0,
68
104
  textStyle = void 0,
105
+ behavior = "smooth",
69
106
  transitionMode = "swoop",
107
+ disabled = false,
108
+ accessibilityLabel = void 0,
109
+ testID = void 0,
70
110
  onPress = void 0,
111
+ onClick = void 0,
71
112
  ...pressableProps
72
113
  }) {
73
114
  const { canFocus, focusCard } = useOverlappingCardsScrollRNCardControl();
74
115
  const handlePress = (event) => {
116
+ onClick == null ? void 0 : onClick(event);
75
117
  onPress == null ? void 0 : onPress(event);
76
- focusCard({ animated: true, transitionMode });
118
+ focusCard({
119
+ animated: behavior !== "auto",
120
+ transitionMode
121
+ });
77
122
  };
78
123
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
79
124
  import_react_native.Pressable,
80
125
  {
81
126
  style: ({ pressed }) => [styles.focusTrigger, pressed && styles.focusTriggerPressed, style],
82
- disabled: !canFocus,
127
+ disabled: disabled || !canFocus,
128
+ accessibilityLabel,
129
+ testID,
83
130
  onPress: handlePress,
84
131
  ...pressableProps,
85
132
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.Text, { style: [styles.focusTriggerText, textStyle], children })
86
133
  }
87
134
  );
88
135
  }
136
+ function DefaultTabsContainerComponent({
137
+ children,
138
+ style,
139
+ ariaLabel
140
+ }) {
141
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.View, { accessibilityRole: "tablist", accessibilityLabel: ariaLabel, style, children });
142
+ }
143
+ function DefaultTabsComponent({
144
+ name,
145
+ style,
146
+ textStyle,
147
+ accessibilityLabel,
148
+ accessibilityState,
149
+ onPress,
150
+ onClick
151
+ }) {
152
+ const handlePress = () => {
153
+ onClick();
154
+ onPress();
155
+ };
156
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
157
+ import_react_native.Pressable,
158
+ {
159
+ accessibilityRole: "tab",
160
+ accessibilityLabel,
161
+ accessibilityState,
162
+ onPress: handlePress,
163
+ style: ({ pressed }) => [styles.tab, pressed && styles.tabPressed, style],
164
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.Text, { style: [styles.tabText, textStyle], children: name })
165
+ }
166
+ );
167
+ }
89
168
  var resolveCardWidth = (cardWidth, viewportWidth, fallbackRatio) => {
90
169
  if (typeof cardWidth === "number" && Number.isFinite(cardWidth) && cardWidth > 0) {
91
170
  return cardWidth;
@@ -105,26 +184,66 @@ var resolveCardWidth = (cardWidth, viewportWidth, fallbackRatio) => {
105
184
  }
106
185
  return viewportWidth * fallbackRatio;
107
186
  };
108
- function OverlappingCardsScrollRN({
109
- children,
110
- style = void 0,
111
- cardHeight = 300,
112
- cardWidth = void 0,
113
- cardWidthRatio = 1 / 3,
114
- basePeek = 64,
115
- minPeek = 10,
116
- maxPeek = 84,
117
- showsHorizontalScrollIndicator = true,
118
- snapToCardOnRelease = true,
119
- snapDecelerationRate = "normal",
120
- snapDisableIntervalMomentum = false,
121
- showPageDots = false,
122
- pageDotsPosition = "below",
123
- pageDotsOffset = 10,
124
- focusTransitionDuration = 420
125
- }) {
126
- const cards = (0, import_react.useMemo)(() => import_react.Children.toArray(children), [children]);
187
+ function OverlappingCardsScrollRN(props) {
188
+ const {
189
+ style = void 0,
190
+ cardHeight = 300,
191
+ cardWidth = void 0,
192
+ cardWidthRatio = 1 / 3,
193
+ basePeek = 64,
194
+ minPeek = 10,
195
+ maxPeek = 84,
196
+ showsHorizontalScrollIndicator = true,
197
+ snapToCardOnRelease = true,
198
+ snapDecelerationRate = "normal",
199
+ snapDisableIntervalMomentum = false,
200
+ showPageDots = false,
201
+ pageDotsPosition = "below",
202
+ pageDotsOffset = 10,
203
+ focusTransitionDuration = 420,
204
+ cardContainerStyle = void 0,
205
+ showTabs = false,
206
+ tabsPosition = "above",
207
+ tabsOffset = 10,
208
+ tabsComponent: TabsComponent = DefaultTabsComponent,
209
+ tabsContainerComponent: TabsContainerComponent = DefaultTabsContainerComponent
210
+ } = props;
211
+ const hasItems = "items" in props && Array.isArray(props.items);
212
+ const hasChildren = "children" in props && props.children != null;
213
+ (0, import_react.useEffect)(() => {
214
+ if (hasItems && hasChildren) {
215
+ console.warn(
216
+ "OverlappingCardsScrollRN: Both `items` and `children` were provided. `items` takes precedence."
217
+ );
218
+ }
219
+ }, [hasItems, hasChildren]);
220
+ const itemsProp = hasItems ? props.items : null;
221
+ const childrenProp = hasChildren ? props.children : null;
222
+ const cards = (0, import_react.useMemo)(() => {
223
+ if (itemsProp) {
224
+ return itemsProp.map((item) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react.Fragment, { children: item.jsx }, item.id));
225
+ }
226
+ return import_react.Children.toArray(childrenProp);
227
+ }, [childrenProp, itemsProp]);
228
+ const cardNames = (0, import_react.useMemo)(() => {
229
+ if (itemsProp) {
230
+ return itemsProp.map((item) => item.name);
231
+ }
232
+ return null;
233
+ }, [itemsProp]);
127
234
  const cardCount = cards.length;
235
+ const resolvedTabsPosition = normalizeTabsPosition(tabsPosition);
236
+ const showNavigationTabs = showTabs && cardCount > 1 && cardNames !== null;
237
+ const resolvedPageDotsOffset = toNumericOffset(pageDotsOffset, 10);
238
+ const resolvedTabsOffset = toNumericOffset(tabsOffset, 10);
239
+ const resolvedCardHeight = toNativeDimension(cardHeight, 300);
240
+ (0, import_react.useEffect)(() => {
241
+ if (showTabs && cardNames === null) {
242
+ console.warn(
243
+ "OverlappingCardsScrollRN: `showTabs` requires the `items` prop to provide card names. Tabs will not render."
244
+ );
245
+ }
246
+ }, [cardNames, showTabs]);
128
247
  const scrollRef = (0, import_react.useRef)(null);
129
248
  const scrollX = (0, import_react.useRef)(new import_react_native.Animated.Value(0)).current;
130
249
  const scrollXValueRef = (0, import_react.useRef)(0);
@@ -133,6 +252,7 @@ function OverlappingCardsScrollRN({
133
252
  const focusTransitionIdRef = (0, import_react.useRef)(0);
134
253
  const [viewportWidth, setViewportWidth] = (0, import_react.useState)(1);
135
254
  const [focusTransition, setFocusTransition] = (0, import_react.useState)(null);
255
+ const [scrollProgress, setScrollProgress] = (0, import_react.useState)(0);
136
256
  const layout = (0, import_react.useMemo)(() => {
137
257
  const safeWidth = Math.max(1, viewportWidth);
138
258
  const safeRatio = clamp(cardWidthRatio, 0.2, 0.95);
@@ -175,11 +295,26 @@ function OverlappingCardsScrollRN({
175
295
  (0, import_react.useEffect)(() => {
176
296
  const id = scrollX.addListener(({ value }) => {
177
297
  scrollXValueRef.current = value;
298
+ if (!showNavigationTabs) {
299
+ return;
300
+ }
301
+ const nextProgress = cardCount > 1 ? clamp(value / layout.stepDistance, 0, cardCount - 1) : 0;
302
+ setScrollProgress(
303
+ (currentProgress) => Math.abs(currentProgress - nextProgress) < 1e-3 ? currentProgress : nextProgress
304
+ );
178
305
  });
179
306
  return () => {
180
307
  scrollX.removeListener(id);
181
308
  };
182
- }, [scrollX]);
309
+ }, [cardCount, layout.stepDistance, scrollX, showNavigationTabs]);
310
+ (0, import_react.useEffect)(() => {
311
+ if (!showNavigationTabs) {
312
+ setScrollProgress(0);
313
+ return;
314
+ }
315
+ const nextProgress = cardCount > 1 ? clamp(scrollXValueRef.current / layout.stepDistance, 0, cardCount - 1) : 0;
316
+ setScrollProgress(nextProgress);
317
+ }, [cardCount, layout.stepDistance, showNavigationTabs]);
183
318
  (0, import_react.useEffect)(() => () => stopFocusTransitionAnimation(), [stopFocusTransitionAnimation]);
184
319
  (0, import_react.useEffect)(() => {
185
320
  if (cardCount > 1) {
@@ -214,6 +349,9 @@ function OverlappingCardsScrollRN({
214
349
  const nextScrollLeft = clamp(safeIndex * layout.stepDistance, 0, layout.scrollRange);
215
350
  const transitionMode = (_a = options.transitionMode) != null ? _a : "swoop";
216
351
  if (transitionMode === "swoop" && cardCount > 1) {
352
+ if (showNavigationTabs) {
353
+ setScrollProgress(safeIndex);
354
+ }
217
355
  const fromProgress = clamp(
218
356
  scrollXValueRef.current / layout.stepDistance,
219
357
  0,
@@ -270,6 +408,9 @@ function OverlappingCardsScrollRN({
270
408
  if (((_c = options.animated) != null ? _c : true) === false) {
271
409
  scrollX.setValue(nextScrollLeft);
272
410
  scrollXValueRef.current = nextScrollLeft;
411
+ if (showNavigationTabs) {
412
+ setScrollProgress(safeIndex);
413
+ }
273
414
  }
274
415
  },
275
416
  [
@@ -280,6 +421,7 @@ function OverlappingCardsScrollRN({
280
421
  layout.scrollRange,
281
422
  layout.stepDistance,
282
423
  scrollX,
424
+ showNavigationTabs,
283
425
  stopFocusTransitionAnimation
284
426
  ]
285
427
  );
@@ -304,11 +446,13 @@ function OverlappingCardsScrollRN({
304
446
  extrapolate: "clamp"
305
447
  });
306
448
  }, [focusTransition, focusTransitionProgress, layout.stepDistance, scrollX]);
449
+ const progress = showNavigationTabs ? scrollProgress : 0;
450
+ const activeIndex = Math.floor(progress);
307
451
  const renderPageDots = (placement) => {
308
452
  if (!showNavigationDots || resolvedPageDotsPosition !== placement) {
309
453
  return null;
310
454
  }
311
- const rowStyle = placement === "above" ? [styles.pageDotsRow, { marginBottom: pageDotsOffset }] : placement === "below" ? [styles.pageDotsRow, { marginTop: pageDotsOffset }] : [styles.pageDotsRow, styles.pageDotsOverlay, { bottom: pageDotsOffset }];
455
+ const rowStyle = placement === "above" ? [styles.pageDotsRow, { marginBottom: resolvedPageDotsOffset }] : placement === "below" ? [styles.pageDotsRow, { marginTop: resolvedPageDotsOffset }] : [styles.pageDotsRow, styles.pageDotsOverlay, { bottom: resolvedPageDotsOffset }];
312
456
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
313
457
  import_react_native.View,
314
458
  {
@@ -344,12 +488,63 @@ function OverlappingCardsScrollRN({
344
488
  }
345
489
  );
346
490
  };
491
+ const renderTabs = (position) => {
492
+ if (!showNavigationTabs || resolvedTabsPosition !== position || cardNames === null) {
493
+ return null;
494
+ }
495
+ const containerStyle = position === "above" ? [styles.tabsRow, { marginBottom: resolvedTabsOffset }] : [styles.tabsRow, { marginTop: resolvedTabsOffset }];
496
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
497
+ TabsContainerComponent,
498
+ {
499
+ position,
500
+ className: `rn-ocs-tabs rn-ocs-tabs--${position}`,
501
+ style: containerStyle,
502
+ ariaLabel: "Card tabs",
503
+ cardNames,
504
+ activeIndex,
505
+ progress,
506
+ children: cardNames.map((name, index) => {
507
+ const influence = clamp(1 - Math.abs(progress - index), 0, 1);
508
+ const isPrincipal = influence > 0.98;
509
+ const animate = {
510
+ opacity: 0.45 + influence * 0.55
511
+ };
512
+ const pressTab = () => focusCard(index, {
513
+ animated: true,
514
+ transitionMode: "swoop"
515
+ });
516
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
517
+ TabsComponent,
518
+ {
519
+ name,
520
+ index,
521
+ position,
522
+ isPrincipal,
523
+ influence,
524
+ animate,
525
+ className: isPrincipal ? "rn-ocs-tab rn-ocs-tab--active" : "rn-ocs-tab",
526
+ style: { opacity: animate.opacity },
527
+ textStyle: isPrincipal ? styles.tabTextActive : void 0,
528
+ ariaLabel: `Go to ${name}`,
529
+ ariaCurrent: isPrincipal ? "page" : void 0,
530
+ accessibilityLabel: `Go to ${name}`,
531
+ accessibilityState: { selected: isPrincipal },
532
+ onPress: pressTab,
533
+ onClick: pressTab
534
+ },
535
+ `rn-ocs-tab-${position}-${index}`
536
+ );
537
+ })
538
+ }
539
+ );
540
+ };
347
541
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(OverlappingCardsScrollRNControllerContext.Provider, { value: controllerContextValue, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react_native.View, { style: [styles.shell, style], children: [
542
+ renderTabs("above"),
348
543
  renderPageDots("above"),
349
544
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
350
545
  import_react_native.View,
351
546
  {
352
- style: [styles.root, { height: cardHeight }],
547
+ style: [styles.root, { height: resolvedCardHeight }],
353
548
  onLayout: (event) => {
354
549
  const width = event.nativeEvent.layout.width || 1;
355
550
  setViewportWidth(Math.max(1, width));
@@ -360,8 +555,8 @@ function OverlappingCardsScrollRN({
360
555
  {
361
556
  ref: scrollRef,
362
557
  horizontal: true,
363
- style: [styles.scrollRegion, { height: cardHeight }],
364
- contentContainerStyle: { width: layout.trackWidth, height: cardHeight },
558
+ style: [styles.scrollRegion, { height: resolvedCardHeight }],
559
+ contentContainerStyle: { width: layout.trackWidth, height: resolvedCardHeight },
365
560
  onScroll,
366
561
  onScrollBeginDrag: cancelFocusTransition,
367
562
  onMomentumScrollBegin: cancelFocusTransition,
@@ -371,7 +566,7 @@ function OverlappingCardsScrollRN({
371
566
  snapToAlignment: shouldSnapToCard ? "start" : void 0,
372
567
  decelerationRate: shouldSnapToCard ? snapDecelerationRate : "normal",
373
568
  disableIntervalMomentum: shouldSnapToCard ? snapDisableIntervalMomentum : false,
374
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.View, { style: [styles.track, { width: layout.trackWidth, height: cardHeight }], children: cards.map((card, index) => {
569
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react_native.View, { style: [styles.track, { width: layout.trackWidth, height: resolvedCardHeight }], children: cards.map((card, index) => {
375
570
  var _a;
376
571
  const restingRightX = index === 0 ? 0 : (index - 1) * layout.peek + layout.cardWidth;
377
572
  const restingLeftX = index * layout.peek;
@@ -396,13 +591,14 @@ function OverlappingCardsScrollRN({
396
591
  styles.card,
397
592
  {
398
593
  width: layout.cardWidth,
399
- height: cardHeight,
594
+ height: resolvedCardHeight,
400
595
  transform: [
401
596
  {
402
597
  translateX: import_react_native.Animated.add(scrollX, animatedCardX)
403
598
  }
404
599
  ]
405
- }
600
+ },
601
+ cardContainerStyle
406
602
  ],
407
603
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(OverlappingCardsScrollRNCardIndexContext.Provider, { value: index, children: card })
408
604
  },
@@ -415,7 +611,8 @@ function OverlappingCardsScrollRN({
415
611
  ]
416
612
  }
417
613
  ),
418
- renderPageDots("below")
614
+ renderPageDots("below"),
615
+ renderTabs("below")
419
616
  ] }) });
420
617
  }
421
618
  var styles = import_react_native.StyleSheet.create({
@@ -466,6 +663,36 @@ var styles = import_react_native.StyleSheet.create({
466
663
  borderRadius: 999,
467
664
  backgroundColor: "#1f4666"
468
665
  },
666
+ tabsRow: {
667
+ width: "100%",
668
+ flexDirection: "row",
669
+ alignItems: "center",
670
+ justifyContent: "center",
671
+ flexWrap: "wrap",
672
+ zIndex: 6
673
+ },
674
+ tab: {
675
+ borderRadius: 999,
676
+ borderWidth: 1,
677
+ borderColor: "rgba(30, 67, 99, 0.2)",
678
+ backgroundColor: "#eef5ff",
679
+ paddingHorizontal: 12,
680
+ paddingVertical: 6,
681
+ marginHorizontal: 4,
682
+ marginVertical: 4
683
+ },
684
+ tabPressed: {
685
+ opacity: 0.85
686
+ },
687
+ tabText: {
688
+ color: "#275070",
689
+ fontSize: 12,
690
+ fontWeight: "700",
691
+ letterSpacing: 0.2
692
+ },
693
+ tabTextActive: {
694
+ color: "#173047"
695
+ },
469
696
  focusTrigger: {
470
697
  alignSelf: "flex-start",
471
698
  borderRadius: 99,