@orionarm/react-native-collapse-tabs 1.0.1 → 1.0.3

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.
Files changed (69) hide show
  1. package/README.md +245 -1
  2. package/lib/CollapseTabs.d.ts +1 -5
  3. package/lib/CollapseTabs.d.ts.map +1 -1
  4. package/lib/CollapseTabs.js +124 -11
  5. package/lib/CollapseTabs.js.map +1 -0
  6. package/lib/FlatList.d.ts +8 -0
  7. package/lib/FlatList.d.ts.map +1 -0
  8. package/lib/FlatList.js +23 -0
  9. package/lib/ScrollView.d.ts +9 -0
  10. package/lib/ScrollView.d.ts.map +1 -0
  11. package/lib/ScrollView.js +25 -0
  12. package/lib/Tab.d.ts +8 -0
  13. package/lib/Tab.d.ts.map +1 -0
  14. package/lib/Tab.js +12 -0
  15. package/lib/TabBar.d.ts +4 -0
  16. package/lib/TabBar.d.ts.map +1 -0
  17. package/lib/TabBar.js +49 -0
  18. package/lib/context/index.d.ts +5 -0
  19. package/lib/context/index.d.ts.map +1 -0
  20. package/lib/context/index.js +2 -0
  21. package/lib/hooks/index.d.ts +3 -0
  22. package/lib/hooks/index.d.ts.map +1 -0
  23. package/lib/hooks/index.js +8 -0
  24. package/lib/index.d.ts +7 -2
  25. package/lib/index.d.ts.map +1 -1
  26. package/lib/index.js +6 -5
  27. package/lib/index.js.map +1 -0
  28. package/lib/lib/CollapseTabs.d.ts +4 -0
  29. package/lib/lib/CollapseTabs.d.ts.map +1 -0
  30. package/lib/lib/CollapseTabs.js +156 -0
  31. package/lib/lib/CollapseTabs.js.map +1 -0
  32. package/lib/lib/FlatList.d.ts +8 -0
  33. package/lib/lib/FlatList.d.ts.map +1 -0
  34. package/lib/lib/FlatList.js +59 -0
  35. package/lib/lib/ScrollView.d.ts +9 -0
  36. package/lib/lib/ScrollView.d.ts.map +1 -0
  37. package/lib/lib/ScrollView.js +61 -0
  38. package/lib/lib/Tab.d.ts +8 -0
  39. package/lib/lib/Tab.d.ts.map +1 -0
  40. package/lib/lib/Tab.js +19 -0
  41. package/lib/lib/TabBar.d.ts +4 -0
  42. package/lib/lib/TabBar.d.ts.map +1 -0
  43. package/lib/lib/TabBar.js +89 -0
  44. package/lib/lib/context/index.d.ts +5 -0
  45. package/lib/lib/context/index.d.ts.map +1 -0
  46. package/lib/lib/context/index.js +8 -0
  47. package/lib/lib/hooks/index.d.ts +3 -0
  48. package/lib/lib/hooks/index.d.ts.map +1 -0
  49. package/lib/lib/hooks/index.js +11 -0
  50. package/lib/lib/index.d.ts +8 -0
  51. package/lib/lib/index.d.ts.map +1 -0
  52. package/lib/lib/index.js +15 -0
  53. package/lib/lib/index.js.map +1 -0
  54. package/lib/lib/types/types.d.ts +57 -0
  55. package/lib/lib/types/types.d.ts.map +1 -0
  56. package/lib/lib/types/types.js +2 -0
  57. package/lib/types/types.d.ts +57 -0
  58. package/lib/types/types.d.ts.map +1 -0
  59. package/lib/types/types.js +1 -0
  60. package/package.json +9 -5
  61. package/src/CollapseTabs.tsx +219 -8
  62. package/src/FlatList.tsx +47 -0
  63. package/src/ScrollView.tsx +54 -0
  64. package/src/Tab.tsx +15 -0
  65. package/src/TabBar.tsx +86 -0
  66. package/src/context/index.tsx +8 -0
  67. package/src/hooks/index.ts +9 -0
  68. package/src/index.tsx +14 -2
  69. package/src/types/types.ts +85 -0
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@orionarm/react-native-collapse-tabs",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "A React Native collapsible tabs component",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
7
7
  "scripts": {
8
8
  "build": "tsc",
9
- "prepare": "npm run build",
9
+ "watch": "tsc --watch",
10
+ "prepublishOnly": "npm run build",
10
11
  "test": "echo \"Error: no test specified\" && exit 1"
11
12
  },
12
13
  "keywords": [
@@ -28,17 +29,20 @@
28
29
  "license": "ISC",
29
30
  "peerDependencies": {
30
31
  "react": ">=16.8.0",
31
- "react-native": ">=0.60.0"
32
+ "react-native": ">=0.60.0",
33
+ "react-native-gesture-handler": ">=2.0.0",
34
+ "react-native-pager-view": ">=6.0.0",
35
+ "react-native-reanimated": ">=3.0.0"
32
36
  },
33
37
  "devDependencies": {
34
38
  "@types/react": "^18.0.0",
35
39
  "@types/react-native": "^0.72.0",
36
40
  "react": "^18.2.0",
37
41
  "react-native": "^0.72.0",
38
- "typescript": "^5.0.0",
39
42
  "react-native-gesture-handler": "~2.16.0",
40
43
  "react-native-pager-view": "^6.3.0",
41
- "react-native-reanimated": "3.8.1"
44
+ "react-native-reanimated": "3.8.1",
45
+ "typescript": "^5.9.3"
42
46
  },
43
47
  "files": [
44
48
  "lib",
@@ -1,14 +1,225 @@
1
- import React from "react";
2
- import { View, StyleSheet, ViewStyle, Text } from "react-native";
1
+ import React, {
2
+ useCallback,
3
+ useMemo,
4
+ useRef,
5
+ useState,
6
+ } from "react";
7
+ import { StyleSheet, View } from "react-native";
8
+ import PagerView, {
9
+ PagerViewOnPageScrollEventData,
10
+ PagerViewOnPageSelectedEventData,
11
+ } from "react-native-pager-view";
12
+ import Animated, {
13
+ AnimatedRef,
14
+ runOnJS,
15
+ useAnimatedReaction,
16
+ useAnimatedStyle,
17
+ useEvent,
18
+ useHandler,
19
+ useSharedValue,
20
+ withTiming,
21
+ } from "react-native-reanimated";
22
+ import { Context } from "./context";
23
+ import { DefaultTabBar } from "./TabBar";
24
+ import {
25
+ CollapseTabsProps,
26
+ ContainerRef,
27
+ ContextType,
28
+ RefComponent,
29
+ TabName,
30
+ TabProps,
31
+ TabReactElement,
32
+ } from "./types/types";
3
33
 
4
- export interface CollapseTabsProps {
5
- children?: React.ReactNode;
6
- style?: ViewStyle;
7
- }
34
+ const AnimatedPagerView = Animated.createAnimatedComponent(PagerView);
8
35
 
9
36
  export const CollapseTabs: React.FC<CollapseTabsProps> = ({
10
37
  children,
11
- style,
38
+ initialTabName,
39
+ headerHeight,
40
+ tabBarHeight,
41
+ minHeaderHeight = 0,
42
+ renderHeader,
43
+ renderTabBar,
44
+ containerStyle,
45
+ headerContainerStyle,
46
+ pagerProps,
47
+ onIndexChange,
12
48
  }) => {
13
- return <View> {children}</View>;
49
+ const tabs = useMemo(
50
+ () =>
51
+ React.Children.toArray(children).filter(
52
+ (c): c is TabReactElement => React.isValidElement(c)
53
+ ),
54
+ [children]
55
+ );
56
+
57
+ const tabNames = useMemo(
58
+ () => tabs.map((t) => (t.props as TabProps).name),
59
+ [tabs]
60
+ );
61
+
62
+ const initialIndex = Math.max(
63
+ 0,
64
+ initialTabName ? tabNames.indexOf(initialTabName) : 0
65
+ );
66
+
67
+ const headerScrollDistance = headerHeight - minHeaderHeight;
68
+
69
+ const index = useSharedValue(initialIndex);
70
+ const indexDecimal = useSharedValue(initialIndex);
71
+ const focusedTab = useSharedValue<TabName>(tabNames[initialIndex] ?? "");
72
+
73
+ const initialScrollY = useMemo(() => {
74
+ const m: Record<string, number> = {};
75
+ tabNames.forEach((n) => (m[n] = 0));
76
+ return m;
77
+ }, [tabNames]);
78
+ const scrollY = useSharedValue<Record<TabName, number>>(initialScrollY);
79
+
80
+ const headerTranslateY = useSharedValue(0);
81
+
82
+ useAnimatedReaction(
83
+ () => {
84
+ const tab = focusedTab.value;
85
+ const y = scrollY.value[tab] ?? 0;
86
+ return {
87
+ tab,
88
+ translateY: -Math.min(y, headerScrollDistance),
89
+ };
90
+ },
91
+ (next, prev) => {
92
+ if (prev && prev.tab !== next.tab) {
93
+ headerTranslateY.value = withTiming(next.translateY, { duration: 250 });
94
+ } else {
95
+ headerTranslateY.value = next.translateY;
96
+ }
97
+ },
98
+ [headerScrollDistance]
99
+ );
100
+
101
+ const refMap = useRef<Record<TabName, AnimatedRef<RefComponent>>>({}).current;
102
+
103
+ const setRef = useCallback(
104
+ <R extends RefComponent>(key: TabName, ref: AnimatedRef<R>) => {
105
+ refMap[key] = ref as unknown as AnimatedRef<RefComponent>;
106
+ return ref;
107
+ },
108
+ [refMap]
109
+ );
110
+
111
+ const containerRef = useRef<ContainerRef>(null);
112
+
113
+ const onTabPress = useCallback((name: TabName) => {
114
+ const i = tabNames.indexOf(name);
115
+ if (i < 0) return;
116
+ containerRef.current?.setPage(i);
117
+ }, [tabNames]);
118
+
119
+ const [renderIndex, setRenderIndex] = useState(initialIndex);
120
+
121
+ const { doDependenciesDiffer } = useHandler({});
122
+ const pageScrollHandler = useEvent<PagerViewOnPageScrollEventData>(
123
+ (e) => {
124
+ "worklet";
125
+ indexDecimal.value = e.position + e.offset;
126
+ },
127
+ ["onPageScroll"],
128
+ doDependenciesDiffer
129
+ );
130
+
131
+ const onPageSelected = useCallback(
132
+ (e: { nativeEvent: PagerViewOnPageSelectedEventData }) => {
133
+ const i = e.nativeEvent.position;
134
+ index.value = i;
135
+ focusedTab.value = tabNames[i] ?? "";
136
+ setRenderIndex(i);
137
+ onIndexChange?.(i);
138
+ },
139
+ [tabNames, index, focusedTab, onIndexChange]
140
+ );
141
+
142
+ const headerAnimStyle = useAnimatedStyle(() => ({
143
+ transform: [{ translateY: headerTranslateY.value }],
144
+ }));
145
+
146
+ const ctxValue: ContextType = {
147
+ headerHeight,
148
+ tabBarHeight,
149
+ minHeaderHeight,
150
+ headerScrollDistance,
151
+ tabNames,
152
+ index,
153
+ indexDecimal,
154
+ focusedTab,
155
+ scrollY,
156
+ headerTranslateY,
157
+ setRef,
158
+ refMap,
159
+ containerRef,
160
+ };
161
+
162
+ const headerProps = {
163
+ tabNames,
164
+ focusedTab,
165
+ index,
166
+ indexDecimal,
167
+ onTabPress,
168
+ };
169
+
170
+ return (
171
+ <Context.Provider value={ctxValue}>
172
+ <View style={[styles.container, containerStyle]}>
173
+ <AnimatedPagerView
174
+ ref={containerRef as any}
175
+ style={StyleSheet.absoluteFill}
176
+ initialPage={initialIndex}
177
+ onPageScroll={pageScrollHandler as any}
178
+ onPageSelected={onPageSelected}
179
+ {...pagerProps}
180
+ >
181
+ {tabs.map((tab, i) => (
182
+ <View key={tab.props.name} style={styles.page} collapsable={false}>
183
+ {tab}
184
+ </View>
185
+ ))}
186
+ </AnimatedPagerView>
187
+
188
+ <Animated.View
189
+ pointerEvents="box-none"
190
+ style={[
191
+ styles.headerContainer,
192
+ { height: headerHeight + tabBarHeight },
193
+ headerContainerStyle,
194
+ headerAnimStyle,
195
+ ]}
196
+ >
197
+ <View
198
+ pointerEvents="box-none"
199
+ style={{ height: headerHeight }}
200
+ >
201
+ {renderHeader?.(headerProps)}
202
+ </View>
203
+ <View style={{ height: tabBarHeight }}>
204
+ {renderTabBar
205
+ ? renderTabBar(headerProps)
206
+ : <DefaultTabBar {...headerProps} />}
207
+ </View>
208
+ </Animated.View>
209
+ </View>
210
+ </Context.Provider>
211
+ );
14
212
  };
213
+
214
+ const styles = StyleSheet.create({
215
+ container: { flex: 1, overflow: "hidden" },
216
+ page: { flex: 1 },
217
+ headerContainer: {
218
+ position: "absolute",
219
+ top: 0,
220
+ left: 0,
221
+ right: 0,
222
+ zIndex: 10,
223
+ backgroundColor: "#fff",
224
+ },
225
+ });
@@ -0,0 +1,47 @@
1
+ import React, { useEffect } from "react";
2
+ import { FlatList as RNFlatList, FlatListProps } from "react-native";
3
+ import Animated, {
4
+ useAnimatedRef,
5
+ useAnimatedScrollHandler,
6
+ } from "react-native-reanimated";
7
+ import { useTabsContext } from "./hooks";
8
+
9
+ type Props<T> = FlatListProps<T> & { name: string };
10
+
11
+ export function FlatList<T>({ name, contentContainerStyle, ...rest }: Props<T>) {
12
+ const {
13
+ headerHeight,
14
+ tabBarHeight,
15
+ scrollY,
16
+ focusedTab,
17
+ setRef,
18
+ } = useTabsContext();
19
+
20
+ const ref = useAnimatedRef<RNFlatList<T>>();
21
+
22
+ useEffect(() => {
23
+ setRef(name, ref);
24
+ }, [name, ref, setRef]);
25
+
26
+ const handler = useAnimatedScrollHandler({
27
+ onScroll: (event) => {
28
+ if (focusedTab.value !== name) return;
29
+ const map = { ...scrollY.value };
30
+ map[name] = event.contentOffset.y;
31
+ scrollY.value = map;
32
+ },
33
+ });
34
+
35
+ return (
36
+ <Animated.FlatList
37
+ {...(rest as FlatListProps<T>)}
38
+ ref={ref as any}
39
+ onScroll={handler}
40
+ scrollEventThrottle={16}
41
+ contentContainerStyle={[
42
+ { paddingTop: headerHeight + tabBarHeight },
43
+ contentContainerStyle,
44
+ ]}
45
+ />
46
+ );
47
+ }
@@ -0,0 +1,54 @@
1
+ import React, { useEffect } from "react";
2
+ import { ScrollViewProps, ScrollView as RNScrollView } from "react-native";
3
+ import Animated, {
4
+ useAnimatedRef,
5
+ useAnimatedScrollHandler,
6
+ } from "react-native-reanimated";
7
+ import { useTabsContext } from "./hooks";
8
+
9
+ type Props = ScrollViewProps & { name: string; children?: React.ReactNode };
10
+
11
+ export function ScrollView({
12
+ name,
13
+ contentContainerStyle,
14
+ children,
15
+ ...rest
16
+ }: Props) {
17
+ const {
18
+ headerHeight,
19
+ tabBarHeight,
20
+ scrollY,
21
+ focusedTab,
22
+ setRef,
23
+ } = useTabsContext();
24
+
25
+ const ref = useAnimatedRef<Animated.ScrollView>();
26
+
27
+ useEffect(() => {
28
+ setRef(name, ref);
29
+ }, [name, ref, setRef]);
30
+
31
+ const handler = useAnimatedScrollHandler({
32
+ onScroll: (event) => {
33
+ if (focusedTab.value !== name) return;
34
+ const map = { ...scrollY.value };
35
+ map[name] = event.contentOffset.y;
36
+ scrollY.value = map;
37
+ },
38
+ });
39
+
40
+ return (
41
+ <Animated.ScrollView
42
+ {...rest}
43
+ ref={ref}
44
+ onScroll={handler}
45
+ scrollEventThrottle={16}
46
+ contentContainerStyle={[
47
+ { paddingTop: headerHeight + tabBarHeight },
48
+ contentContainerStyle,
49
+ ]}
50
+ >
51
+ {children}
52
+ </Animated.ScrollView>
53
+ );
54
+ }
package/src/Tab.tsx ADDED
@@ -0,0 +1,15 @@
1
+ import React from "react";
2
+ import { View, StyleSheet } from "react-native";
3
+ import { TabProps } from "./types/types";
4
+
5
+ /**
6
+ * Container for a single tab's content. Should be a direct child of
7
+ * <CollapseTabs>. Place a wrapped <FlatList> or <ScrollView> inside.
8
+ */
9
+ export const Tab = <T extends string>({ children }: TabProps<T>) => {
10
+ return <View style={styles.container}>{children}</View>;
11
+ };
12
+
13
+ const styles = StyleSheet.create({
14
+ container: { flex: 1 },
15
+ });
package/src/TabBar.tsx ADDED
@@ -0,0 +1,86 @@
1
+ import React from "react";
2
+ import { Pressable, StyleSheet, Text, View } from "react-native";
3
+ import Animated, {
4
+ interpolate,
5
+ useAnimatedStyle,
6
+ } from "react-native-reanimated";
7
+ import { TabBarProps } from "./types/types";
8
+
9
+ export const DefaultTabBar: React.FC<TabBarProps> = ({
10
+ tabNames,
11
+ indexDecimal,
12
+ onTabPress,
13
+ }) => {
14
+ return (
15
+ <View style={styles.container}>
16
+ {tabNames.map((name, i) => (
17
+ <TabBarItem
18
+ key={name}
19
+ name={name}
20
+ index={i}
21
+ indexDecimal={indexDecimal}
22
+ onPress={() => onTabPress(name)}
23
+ />
24
+ ))}
25
+ <Indicator count={tabNames.length} indexDecimal={indexDecimal} />
26
+ </View>
27
+ );
28
+ };
29
+
30
+ const TabBarItem: React.FC<{
31
+ name: string;
32
+ index: number;
33
+ indexDecimal: Animated.SharedValue<number>;
34
+ onPress: () => void;
35
+ }> = ({ name, index, indexDecimal, onPress }) => {
36
+ const labelStyle = useAnimatedStyle(() => ({
37
+ opacity: interpolate(
38
+ indexDecimal.value,
39
+ [index - 1, index, index + 1],
40
+ [0.6, 1, 0.6],
41
+ "clamp"
42
+ ),
43
+ }));
44
+ return (
45
+ <Pressable style={styles.item} onPress={onPress}>
46
+ <Animated.Text style={[styles.label, labelStyle]}>{name}</Animated.Text>
47
+ </Pressable>
48
+ );
49
+ };
50
+
51
+ const Indicator: React.FC<{
52
+ count: number;
53
+ indexDecimal: Animated.SharedValue<number>;
54
+ }> = ({ count, indexDecimal }) => {
55
+ const style = useAnimatedStyle(() => ({
56
+ transform: [
57
+ { translateX: (indexDecimal.value * 100) / count + "%" } as any,
58
+ ],
59
+ width: `${100 / count}%`,
60
+ }));
61
+ return <Animated.View style={[styles.indicator, style]} />;
62
+ };
63
+
64
+ const styles = StyleSheet.create({
65
+ container: {
66
+ flexDirection: "row",
67
+ backgroundColor: "#fff",
68
+ },
69
+ item: {
70
+ flex: 1,
71
+ alignItems: "center",
72
+ justifyContent: "center",
73
+ },
74
+ label: {
75
+ fontSize: 14,
76
+ fontWeight: "500",
77
+ color: "#333",
78
+ },
79
+ indicator: {
80
+ position: "absolute",
81
+ bottom: 0,
82
+ left: 0,
83
+ height: 2,
84
+ backgroundColor: "#2D8CFF",
85
+ },
86
+ });
@@ -0,0 +1,8 @@
1
+ import React from "react";
2
+ import { ContextType, TabName } from "../types/types";
3
+
4
+ export type { ContextType, TabName, RefComponent } from "../types/types";
5
+
6
+ export const Context = React.createContext<ContextType<TabName> | undefined>(
7
+ undefined
8
+ );
@@ -0,0 +1,9 @@
1
+ import { useContext } from "react";
2
+ import { Context } from "../context";
3
+ import { ContextType } from "../types/types";
4
+
5
+ export function useTabsContext(): ContextType<string> {
6
+ const c = useContext(Context);
7
+ if (!c) throw new Error("useTabsContext must be used inside <CollapseTabs>");
8
+ return c;
9
+ }
package/src/index.tsx CHANGED
@@ -1,2 +1,14 @@
1
- export { CollapseTabs } from './CollapseTabs';
2
- export type { CollapseTabsProps } from './CollapseTabs';
1
+ export { CollapseTabs } from "./CollapseTabs";
2
+ export { Tab } from "./Tab";
3
+ export { FlatList } from "./FlatList";
4
+ export { ScrollView } from "./ScrollView";
5
+ export { DefaultTabBar } from "./TabBar";
6
+ export { useTabsContext } from "./hooks";
7
+
8
+ export type {
9
+ CollapseTabsProps,
10
+ TabProps,
11
+ TabBarProps,
12
+ HeaderProps,
13
+ TabName,
14
+ } from "./types/types";
@@ -0,0 +1,85 @@
1
+ import React from "react";
2
+ import { FlatList, ScrollView, StyleProp, ViewStyle } from "react-native";
3
+ import PagerView, { PagerViewProps } from "react-native-pager-view";
4
+ import Animated, { AnimatedRef, SharedValue } from "react-native-reanimated";
5
+
6
+ export type TabName = string;
7
+
8
+ export type ContainerRef = PagerView;
9
+
10
+ export type RefComponent =
11
+ | FlatList<any>
12
+ | ScrollView
13
+ | Animated.ScrollView;
14
+
15
+ export type TabBarProps<T extends TabName = TabName> = {
16
+ tabNames: T[];
17
+ focusedTab: SharedValue<T>;
18
+ index: SharedValue<number>;
19
+ indexDecimal: SharedValue<number>;
20
+ onTabPress: (name: T) => void;
21
+ };
22
+
23
+ export type HeaderProps<T extends TabName = TabName> = TabBarProps<T>;
24
+
25
+ export type TabProps<T extends TabName = TabName> = {
26
+ readonly name: T;
27
+ label?: string;
28
+ children: React.ReactNode;
29
+ };
30
+
31
+ export type TabReactElement<T extends TabName = TabName> =
32
+ React.ReactElement<TabProps<T>>;
33
+
34
+ export type CollapseTabsProps = {
35
+ children: TabReactElement | TabReactElement[];
36
+
37
+ /** Initial tab name. Defaults to the first child. */
38
+ initialTabName?: TabName;
39
+
40
+ /** Fixed header height. Required for MVP. */
41
+ headerHeight: number;
42
+
43
+ /** Fixed tab bar height. Required for MVP. */
44
+ tabBarHeight: number;
45
+
46
+ /** Minimum header height when fully collapsed. @default 0 */
47
+ minHeaderHeight?: number;
48
+
49
+ renderHeader?: (props: HeaderProps) => React.ReactElement | null;
50
+ renderTabBar?: (props: TabBarProps) => React.ReactElement | null;
51
+
52
+ containerStyle?: StyleProp<ViewStyle>;
53
+ headerContainerStyle?: StyleProp<ViewStyle>;
54
+
55
+ pagerProps?: Omit<PagerViewProps, "onPageScroll" | "initialPage">;
56
+
57
+ onIndexChange?: (index: number) => void;
58
+ };
59
+
60
+ /** Internal context shared between CollapseTabs and its children. */
61
+ export type ContextType<T extends TabName = TabName> = {
62
+ headerHeight: number;
63
+ tabBarHeight: number;
64
+ minHeaderHeight: number;
65
+ headerScrollDistance: number;
66
+
67
+ tabNames: T[];
68
+ index: SharedValue<number>;
69
+ indexDecimal: SharedValue<number>;
70
+ focusedTab: SharedValue<T>;
71
+
72
+ /** Per-tab scroll Y, keyed by tab name. */
73
+ scrollY: SharedValue<Record<T, number>>;
74
+
75
+ /** Header translateY (clamped to [-headerScrollDistance, 0]). */
76
+ headerTranslateY: SharedValue<number>;
77
+
78
+ setRef: <R extends RefComponent>(
79
+ key: T,
80
+ ref: AnimatedRef<R>
81
+ ) => AnimatedRef<R>;
82
+ refMap: Record<T, AnimatedRef<RefComponent>>;
83
+
84
+ containerRef: React.RefObject<ContainerRef>;
85
+ };