@swmansion/react-native-bottom-sheet 0.7.0-next.2 → 0.7.0-next.4

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 (58) hide show
  1. package/android/src/main/java/com/swmansion/reactnativebottomsheet/BottomSheetView.kt +4 -1
  2. package/ios/BottomSheetContentView.mm +6 -0
  3. package/ios/RNSBottomSheetHostingView.swift +44 -0
  4. package/lib/module/BottomSheet.js +101 -38
  5. package/lib/module/BottomSheet.js.map +1 -1
  6. package/lib/module/ModalBottomSheet.js +3 -7
  7. package/lib/module/ModalBottomSheet.js.map +1 -1
  8. package/lib/module/bottomSheetUtils.js +0 -2
  9. package/lib/module/bottomSheetUtils.js.map +1 -1
  10. package/lib/module/index.js +0 -3
  11. package/lib/module/index.js.map +1 -1
  12. package/lib/typescript/src/BottomSheet.d.ts +4 -3
  13. package/lib/typescript/src/BottomSheet.d.ts.map +1 -1
  14. package/lib/typescript/src/ModalBottomSheet.d.ts +3 -6
  15. package/lib/typescript/src/ModalBottomSheet.d.ts.map +1 -1
  16. package/lib/typescript/src/bottomSheetUtils.d.ts.map +1 -1
  17. package/lib/typescript/src/index.d.ts +0 -5
  18. package/lib/typescript/src/index.d.ts.map +1 -1
  19. package/package.json +2 -8
  20. package/src/BottomSheet.tsx +144 -37
  21. package/src/ModalBottomSheet.tsx +5 -13
  22. package/src/bottomSheetUtils.ts +0 -1
  23. package/src/index.tsx +0 -5
  24. package/lib/module/BottomSheetBase.js +0 -207
  25. package/lib/module/BottomSheetBase.js.map +0 -1
  26. package/lib/module/BottomSheetContext.js +0 -13
  27. package/lib/module/BottomSheetContext.js.map +0 -1
  28. package/lib/module/BottomSheetFlatList.js +0 -6
  29. package/lib/module/BottomSheetFlatList.js.map +0 -1
  30. package/lib/module/BottomSheetScrollView.js +0 -6
  31. package/lib/module/BottomSheetScrollView.js.map +0 -1
  32. package/lib/module/bottomSheetScrollable.js +0 -35
  33. package/lib/module/bottomSheetScrollable.js.map +0 -1
  34. package/lib/module/useBottomSheetPanGesture.js +0 -202
  35. package/lib/module/useBottomSheetPanGesture.js.map +0 -1
  36. package/lib/module/useBottomSheetScrollable.js +0 -61
  37. package/lib/module/useBottomSheetScrollable.js.map +0 -1
  38. package/lib/typescript/src/BottomSheetBase.d.ts +0 -20
  39. package/lib/typescript/src/BottomSheetBase.d.ts.map +0 -1
  40. package/lib/typescript/src/BottomSheetContext.d.ts +0 -19
  41. package/lib/typescript/src/BottomSheetContext.d.ts.map +0 -1
  42. package/lib/typescript/src/BottomSheetFlatList.d.ts +0 -10
  43. package/lib/typescript/src/BottomSheetFlatList.d.ts.map +0 -1
  44. package/lib/typescript/src/BottomSheetScrollView.d.ts +0 -10
  45. package/lib/typescript/src/BottomSheetScrollView.d.ts.map +0 -1
  46. package/lib/typescript/src/bottomSheetScrollable.d.ts +0 -9
  47. package/lib/typescript/src/bottomSheetScrollable.d.ts.map +0 -1
  48. package/lib/typescript/src/useBottomSheetPanGesture.d.ts +0 -18
  49. package/lib/typescript/src/useBottomSheetPanGesture.d.ts.map +0 -1
  50. package/lib/typescript/src/useBottomSheetScrollable.d.ts +0 -13
  51. package/lib/typescript/src/useBottomSheetScrollable.d.ts.map +0 -1
  52. package/src/BottomSheetBase.tsx +0 -276
  53. package/src/BottomSheetContext.tsx +0 -33
  54. package/src/BottomSheetFlatList.tsx +0 -21
  55. package/src/BottomSheetScrollView.tsx +0 -22
  56. package/src/bottomSheetScrollable.tsx +0 -42
  57. package/src/useBottomSheetPanGesture.ts +0 -253
  58. package/src/useBottomSheetScrollable.ts +0 -68
@@ -1,276 +0,0 @@
1
- import { useCallback, useEffect, useState } from 'react';
2
- import type { ReactNode } from 'react';
3
- import type { LayoutChangeEvent } from 'react-native';
4
- import { Pressable, StyleSheet, View, useWindowDimensions } from 'react-native';
5
- import type { SharedValue, WithSpringConfig } from 'react-native-reanimated';
6
- import Animated, {
7
- useAnimatedReaction,
8
- useAnimatedStyle,
9
- useDerivedValue,
10
- useSharedValue,
11
- withSpring,
12
- } from 'react-native-reanimated';
13
- import { scheduleOnUI } from 'react-native-worklets';
14
- import { GestureDetector } from 'react-native-gesture-handler';
15
- import { useSafeAreaInsets } from 'react-native-safe-area-context';
16
- import { Portal } from './BottomSheetProvider';
17
- import {
18
- BottomSheetContextProvider,
19
- type ScrollableEntry,
20
- } from './BottomSheetContext';
21
- import {
22
- clampIndex,
23
- isDetentProgrammatic,
24
- resolveDetent,
25
- } from './bottomSheetUtils';
26
- import type { Detent } from './bottomSheetUtils';
27
- import { useBottomSheetPanGesture } from './useBottomSheetPanGesture';
28
- export type { Detent, DetentValue } from './bottomSheetUtils';
29
- export { programmatic } from './bottomSheetUtils';
30
-
31
- export interface BottomSheetCommonProps {
32
- children: ReactNode;
33
- detents?: Detent[];
34
- index: number;
35
- onIndexChange?: (index: number) => void;
36
- position?: SharedValue<number>;
37
- openAnimationConfig?: WithSpringConfig;
38
- closeAnimationConfig?: WithSpringConfig;
39
- }
40
-
41
- export interface BottomSheetBaseProps extends BottomSheetCommonProps {
42
- modal?: boolean;
43
- renderScrim?: (progress: SharedValue<number>) => ReactNode;
44
- }
45
-
46
- const DEFAULT_OPEN_ANIMATION_CONFIG: WithSpringConfig = {
47
- dampingRatio: 1,
48
- duration: 300,
49
- overshootClamping: true,
50
- };
51
-
52
- const DEFAULT_CLOSE_ANIMATION_CONFIG: WithSpringConfig = {
53
- dampingRatio: 1,
54
- duration: 250,
55
- overshootClamping: true,
56
- };
57
-
58
- const DefaultScrim = ({ progress }: { progress: SharedValue<number> }) => {
59
- const style = useAnimatedStyle(() => ({ opacity: progress.value }));
60
- return (
61
- <Animated.View
62
- style={[
63
- StyleSheet.absoluteFill,
64
- { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.5)' },
65
- style,
66
- ]}
67
- />
68
- );
69
- };
70
-
71
- export const BottomSheetBase = ({
72
- children,
73
- detents = [0, 'max'],
74
- index,
75
- onIndexChange,
76
- position: externalPosition,
77
- openAnimationConfig = DEFAULT_OPEN_ANIMATION_CONFIG,
78
- closeAnimationConfig = DEFAULT_CLOSE_ANIMATION_CONFIG,
79
- modal = false,
80
- renderScrim,
81
- }: BottomSheetBaseProps) => {
82
- const { height: screenHeight } = useWindowDimensions();
83
- const insets = useSafeAreaInsets();
84
- const maxHeight = screenHeight - insets.top;
85
- const resolvedIndex = clampIndex(index, detents.length);
86
- const [contentHeight, setContentHeight] = useState(0);
87
-
88
- if (detents.length === 0) {
89
- throw new Error('detents must include at least one value.');
90
- }
91
-
92
- const normalizedDetents = detents.map((detent) => {
93
- const resolved = resolveDetent(detent, contentHeight, maxHeight);
94
- return Math.max(0, Math.min(resolved, maxHeight));
95
- });
96
- const isDraggable = detents.map((detent) => !isDetentProgrammatic(detent));
97
- const initialMaxSnap = Math.max(0, ...normalizedDetents);
98
-
99
- const translateY = useSharedValue(initialMaxSnap);
100
- const animationTarget = useSharedValue(NaN);
101
- const sheetHeight = useSharedValue(initialMaxSnap);
102
- const [scrollableEntries, setScrollableEntries] = useState<ScrollableEntry[]>(
103
- []
104
- );
105
- const isScrollableLocked = useSharedValue(false);
106
-
107
- const registerScrollable = useCallback((entry: ScrollableEntry) => {
108
- setScrollableEntries((prev) => [...prev, entry]);
109
- return () => {
110
- setScrollableEntries((prev) => prev.filter((e) => e !== entry));
111
- };
112
- }, []);
113
-
114
- const detentsValue = useSharedValue(normalizedDetents);
115
- const isDraggableValue = useSharedValue(isDraggable);
116
- const firstNonzeroDetent = useSharedValue(
117
- normalizedDetents.find((detent) => detent > 0) ?? 0
118
- );
119
- const currentIndex = useSharedValue(resolvedIndex);
120
- const internalPosition = useDerivedValue(() =>
121
- Math.max(0, sheetHeight.value - translateY.value)
122
- );
123
-
124
- useAnimatedReaction(
125
- () => internalPosition.value,
126
- (value) => {
127
- if (externalPosition !== undefined) externalPosition.set(value);
128
- }
129
- );
130
-
131
- const scrimProgress = useDerivedValue(() => {
132
- const target = firstNonzeroDetent.value;
133
- if (target <= 0) return 0;
134
- const progress = internalPosition.value / target;
135
- return Math.min(1, Math.max(0, progress));
136
- });
137
-
138
- const handleIndexChange = (nextIndex: number) => {
139
- onIndexChange?.(nextIndex);
140
- };
141
-
142
- useEffect(() => {
143
- const maxSnap = Math.max(0, ...normalizedDetents);
144
- detentsValue.set(normalizedDetents);
145
- isDraggableValue.set(isDraggable);
146
- sheetHeight.set(maxSnap);
147
- firstNonzeroDetent.set(normalizedDetents.find((detent) => detent > 0) ?? 0);
148
- }, [
149
- normalizedDetents,
150
- isDraggable,
151
- sheetHeight,
152
- detentsValue,
153
- isDraggableValue,
154
- firstNonzeroDetent,
155
- ]);
156
-
157
- const animateToIndex = useCallback(
158
- (targetIndex: number, velocity?: number) => {
159
- 'worklet';
160
- const maxSnap = sheetHeight.value;
161
- const targetTranslate = maxSnap - (detentsValue.value[targetIndex] ?? 0);
162
- if (animationTarget.value === targetTranslate && velocity === undefined) {
163
- currentIndex.set(targetIndex);
164
- return;
165
- }
166
- animationTarget.set(targetTranslate);
167
- const isOpening = targetTranslate < translateY.value;
168
- const baseConfig = isOpening ? openAnimationConfig : closeAnimationConfig;
169
- const springConfig =
170
- velocity === undefined ? baseConfig : { ...baseConfig, velocity };
171
- translateY.set(withSpring(targetTranslate, springConfig));
172
- currentIndex.set(targetIndex);
173
- },
174
- [
175
- animationTarget,
176
- closeAnimationConfig,
177
- currentIndex,
178
- detentsValue,
179
- openAnimationConfig,
180
- sheetHeight,
181
- translateY,
182
- ]
183
- );
184
-
185
- useEffect(() => {
186
- scheduleOnUI(animateToIndex, resolvedIndex);
187
- }, [animateToIndex, resolvedIndex, normalizedDetents]);
188
-
189
- const panGesture = useBottomSheetPanGesture({
190
- animationTarget,
191
- translateY,
192
- sheetHeight,
193
- detentsValue,
194
- isDraggableValue,
195
- currentIndex,
196
- scrollableEntries,
197
- isScrollableLocked,
198
- handleIndexChange,
199
- animateToIndex,
200
- });
201
-
202
- const handleSentinelLayout = (event: LayoutChangeEvent) => {
203
- setContentHeight(event.nativeEvent.layout.y);
204
- };
205
- const closedIndex = normalizedDetents.indexOf(0);
206
- const handleScrimPress = () => {
207
- if (closedIndex === -1 || resolvedIndex === closedIndex) return;
208
- handleIndexChange(closedIndex);
209
- scheduleOnUI(animateToIndex, closedIndex);
210
- };
211
-
212
- const wrapperStyle = useAnimatedStyle(() => ({
213
- transform: [{ translateY: translateY.value }],
214
- height: sheetHeight.value,
215
- opacity: translateY.value >= sheetHeight.value ? 0 : 1,
216
- }));
217
- const isCollapsed = normalizedDetents[resolvedIndex] === 0;
218
- const pointerEvents = modal ? (isCollapsed ? 'none' : 'auto') : 'box-none';
219
- let scrimElement: ReactNode | null = null;
220
- if (renderScrim !== undefined) {
221
- scrimElement = renderScrim(scrimProgress);
222
- } else if (modal) {
223
- scrimElement = <DefaultScrim progress={scrimProgress} />;
224
- }
225
-
226
- const sheetContent = (
227
- <BottomSheetContextProvider
228
- value={{
229
- translateY,
230
- position: internalPosition,
231
- index: currentIndex,
232
- sheetHeight,
233
- isScrollableLocked,
234
- registerScrollable,
235
- panGesture,
236
- }}
237
- >
238
- <Animated.View
239
- style={[
240
- {
241
- position: 'absolute',
242
- bottom: 0,
243
- left: 0,
244
- right: 0,
245
- },
246
- wrapperStyle,
247
- ]}
248
- pointerEvents="box-none"
249
- >
250
- <GestureDetector gesture={panGesture}>
251
- <View style={{ flex: 1 }} pointerEvents="box-none">
252
- {children}
253
- <View onLayout={handleSentinelLayout} pointerEvents="none" />
254
- </View>
255
- </GestureDetector>
256
- </Animated.View>
257
- </BottomSheetContextProvider>
258
- );
259
-
260
- const sheetContainer = (
261
- <Animated.View
262
- style={StyleSheet.absoluteFill}
263
- pointerEvents={pointerEvents}
264
- >
265
- {modal && scrimElement !== null ? (
266
- <Pressable style={StyleSheet.absoluteFill} onPress={handleScrimPress}>
267
- {scrimElement}
268
- </Pressable>
269
- ) : null}
270
- {sheetContent}
271
- </Animated.View>
272
- );
273
- if (modal) return <Portal>{sheetContainer}</Portal>;
274
-
275
- return sheetContainer;
276
- };
@@ -1,33 +0,0 @@
1
- import { createContext, useContext } from 'react';
2
- import type { PanGesture } from 'react-native-gesture-handler';
3
- import type { AnimatedRef, SharedValue } from 'react-native-reanimated';
4
-
5
- export interface ScrollableEntry {
6
- ref: AnimatedRef<any>;
7
- scrollOffset: SharedValue<number>;
8
- isGestureActive: SharedValue<boolean>;
9
- }
10
-
11
- export interface BottomSheetContextType {
12
- translateY: SharedValue<number>;
13
- position: SharedValue<number>;
14
- index: SharedValue<number>;
15
- sheetHeight: SharedValue<number>;
16
- isScrollableLocked: SharedValue<boolean>;
17
- registerScrollable: (entry: ScrollableEntry) => () => void;
18
- panGesture: PanGesture;
19
- }
20
-
21
- const BottomSheetContext = createContext<BottomSheetContextType | null>(null);
22
-
23
- export const BottomSheetContextProvider = BottomSheetContext.Provider;
24
-
25
- export const useBottomSheetContext = () => {
26
- const context = useContext(BottomSheetContext);
27
- if (context === null) {
28
- throw new Error(
29
- '`useBottomSheetContext` must be used within `BottomSheet`.'
30
- );
31
- }
32
- return context;
33
- };
@@ -1,21 +0,0 @@
1
- import { FlatList, type NativeScrollEvent } from 'react-native';
2
- import type {
3
- FlatListPropsWithLayout,
4
- SharedValue,
5
- } from 'react-native-reanimated';
6
- import type { Ref, ReactElement } from 'react';
7
-
8
- import { bottomSheetScrollable } from './bottomSheetScrollable';
9
-
10
- export type BottomSheetFlatListProps<T> = Omit<
11
- FlatListPropsWithLayout<T>,
12
- 'onScroll' | 'scrollEnabled' | 'ref'
13
- > & {
14
- scrollEnabled?: boolean | SharedValue<boolean | undefined>;
15
- onScroll?: (event: NativeScrollEvent) => void;
16
- ref?: Ref<FlatList<T>>;
17
- };
18
-
19
- export const BottomSheetFlatList = bottomSheetScrollable(FlatList) as <T>(
20
- props: BottomSheetFlatListProps<T>
21
- ) => ReactElement;
@@ -1,22 +0,0 @@
1
- import type { Ref, ReactElement } from 'react';
2
- import {
3
- ScrollView,
4
- type NativeScrollEvent,
5
- type ScrollViewProps,
6
- } from 'react-native';
7
- import type { SharedValue } from 'react-native-reanimated';
8
-
9
- import { bottomSheetScrollable } from './bottomSheetScrollable';
10
-
11
- export type BottomSheetScrollViewProps = Omit<
12
- ScrollViewProps,
13
- 'onScroll' | 'scrollEnabled' | 'ref'
14
- > & {
15
- scrollEnabled?: boolean | SharedValue<boolean | undefined>;
16
- onScroll?: (event: NativeScrollEvent) => void;
17
- ref?: Ref<ScrollView>;
18
- };
19
-
20
- export const BottomSheetScrollView = bottomSheetScrollable(ScrollView) as (
21
- props: BottomSheetScrollViewProps
22
- ) => ReactElement;
@@ -1,42 +0,0 @@
1
- import { type ComponentType, type Ref, useImperativeHandle } from 'react';
2
- import type { NativeScrollEvent } from 'react-native';
3
- import { GestureDetector } from 'react-native-gesture-handler';
4
- import Animated, { type SharedValue } from 'react-native-reanimated';
5
-
6
- import { useBottomSheetScrollable } from './useBottomSheetScrollable';
7
-
8
- export function bottomSheetScrollable<
9
- P extends Record<string, any>,
10
- R = unknown
11
- >(ScrollableComponent: ComponentType<P>) {
12
- const AnimatedComponent =
13
- Animated.createAnimatedComponent(ScrollableComponent);
14
-
15
- return ({
16
- scrollEnabled,
17
- onScroll,
18
- ref,
19
- ...rest
20
- }: Omit<P, 'onScroll' | 'scrollEnabled' | 'ref'> & {
21
- scrollEnabled?: boolean | SharedValue<boolean | undefined>;
22
- onScroll?: (event: NativeScrollEvent) => void;
23
- ref?: Ref<R>;
24
- }) => {
25
- const { scrollHandler, scrollableRef, nativeGesture, animatedProps } =
26
- useBottomSheetScrollable(scrollEnabled, onScroll);
27
-
28
- useImperativeHandle(ref, () => scrollableRef.current as R, [scrollableRef]);
29
-
30
- return (
31
- <GestureDetector gesture={nativeGesture}>
32
- <AnimatedComponent
33
- {...(rest as any)}
34
- animatedProps={animatedProps}
35
- ref={scrollableRef}
36
- onScroll={scrollHandler}
37
- scrollEventThrottle={16}
38
- />
39
- </GestureDetector>
40
- );
41
- };
42
- }
@@ -1,253 +0,0 @@
1
- import type { PanGesture } from 'react-native-gesture-handler';
2
- import { Gesture } from 'react-native-gesture-handler';
3
- import { scheduleOnRN } from 'react-native-worklets';
4
- import {
5
- measure,
6
- scrollTo,
7
- type SharedValue,
8
- useSharedValue,
9
- } from 'react-native-reanimated';
10
-
11
- import type { ScrollableEntry } from './BottomSheetContext';
12
- import { findSnapTarget } from './bottomSheetUtils';
13
-
14
- interface BottomSheetPanGestureParams {
15
- animationTarget: SharedValue<number>;
16
- translateY: SharedValue<number>;
17
- sheetHeight: SharedValue<number>;
18
- detentsValue: SharedValue<number[]>;
19
- isDraggableValue: SharedValue<boolean[]>;
20
- currentIndex: SharedValue<number>;
21
- scrollableEntries: ScrollableEntry[];
22
- isScrollableLocked: SharedValue<boolean>;
23
- handleIndexChange: (nextIndex: number) => void;
24
- animateToIndex: (targetIndex: number, velocity?: number) => void;
25
- }
26
-
27
- export const useBottomSheetPanGesture = ({
28
- animationTarget,
29
- translateY,
30
- sheetHeight,
31
- detentsValue,
32
- isDraggableValue,
33
- currentIndex,
34
- scrollableEntries,
35
- isScrollableLocked,
36
- handleIndexChange,
37
- animateToIndex,
38
- }: BottomSheetPanGestureParams): PanGesture => {
39
- const isDraggingSheet = useSharedValue(false);
40
- const isDraggingFromScrollable = useSharedValue(false);
41
- const panStartX = useSharedValue(0);
42
- const panStartY = useSharedValue(0);
43
- const panActivated = useSharedValue(false);
44
- const dragStartTranslateY = useSharedValue(0);
45
- const activeScrollableIndex = useSharedValue(-1);
46
-
47
- return Gesture.Pan()
48
- .manualActivation(true)
49
- .onTouchesDown((event) => {
50
- 'worklet';
51
- panActivated.set(false);
52
- isDraggingSheet.set(false);
53
- isDraggingFromScrollable.set(false);
54
- isScrollableLocked.set(false);
55
- activeScrollableIndex.set(-1);
56
- const touch = event.changedTouches[0] ?? event.allTouches[0];
57
- if (touch !== undefined) {
58
- panStartX.set(touch.absoluteX);
59
- panStartY.set(touch.absoluteY);
60
- const entries = scrollableEntries;
61
- for (let i = 0; i < entries.length; i++) {
62
- const entry = entries[i];
63
- if (entry === undefined) continue;
64
- const layout = measure(entry.ref);
65
- if (layout === null) continue;
66
- const withinX =
67
- touch.absoluteX >= layout.pageX &&
68
- touch.absoluteX <= layout.pageX + layout.width;
69
- const withinY =
70
- touch.absoluteY >= layout.pageY &&
71
- touch.absoluteY <= layout.pageY + layout.height;
72
- if (withinX && withinY) {
73
- activeScrollableIndex.set(i);
74
- break;
75
- }
76
- }
77
- }
78
- })
79
- .onTouchesMove((event, stateManager) => {
80
- 'worklet';
81
- if (panActivated.value) return;
82
- const touch = event.changedTouches[0] ?? event.allTouches[0];
83
- if (!touch) return;
84
- const deltaX = touch.absoluteX - panStartX.value;
85
- const deltaY = touch.absoluteY - panStartY.value;
86
- // When multiple scrollables overlap (e.g. stacked views), the hit-test
87
- // in onTouchesDown may pick the wrong one. Prefer the scrollable whose
88
- // native gesture is already active — that is definitively the one
89
- // receiving touches (respects pointerEvents, z-order, etc.).
90
- // If multiple scrollables are registered but none has confirmed via
91
- // isGestureActive yet, defer the decision to avoid acting on a
92
- // potentially incorrect hit-test result.
93
- const entries = scrollableEntries;
94
- let gestureActiveIdx = -1;
95
- for (let i = 0; i < entries.length; i++) {
96
- const entry = entries[i];
97
- if (entry !== undefined && entry.isGestureActive.value) {
98
- gestureActiveIdx = i;
99
- break;
100
- }
101
- }
102
- if (gestureActiveIdx !== -1) {
103
- activeScrollableIndex.set(gestureActiveIdx);
104
- } else if (entries.length > 1 && activeScrollableIndex.value !== -1) {
105
- return;
106
- }
107
- const activeIdx = activeScrollableIndex.value;
108
- if (activeIdx !== -1) {
109
- const active = scrollableEntries[activeIdx];
110
- if (
111
- active !== undefined &&
112
- active.scrollOffset.value > 0 &&
113
- translateY.value <= 0
114
- ) {
115
- return;
116
- }
117
- }
118
- if (Math.abs(deltaX) > Math.abs(deltaY)) {
119
- stateManager.fail();
120
- return;
121
- }
122
- if (
123
- Math.abs(deltaY) > Math.abs(deltaX) &&
124
- (deltaY > 0 || translateY.value > 0)
125
- ) {
126
- panActivated.set(true);
127
- stateManager.activate();
128
- }
129
- })
130
- .onBegin(() => {
131
- 'worklet';
132
- animationTarget.set(NaN);
133
- isDraggingSheet.set(false);
134
- isDraggingFromScrollable.set(false);
135
- dragStartTranslateY.set(translateY.value);
136
- })
137
- .onUpdate((event) => {
138
- 'worklet';
139
- const activeIdx = activeScrollableIndex.value;
140
- const hasActive = activeIdx !== -1;
141
- const active = hasActive ? scrollableEntries[activeIdx] : undefined;
142
- const activeOffset = active !== undefined ? active.scrollOffset.value : 0;
143
-
144
- if (isDraggingSheet.value) {
145
- if (isDraggingFromScrollable.value && active !== undefined) {
146
- scrollTo(active.ref, 0, 0, false);
147
- }
148
- } else {
149
- const isDraggingDown = event.translationY > 0;
150
- const canStartDrag =
151
- !hasActive || activeOffset <= 0 || translateY.value > 0;
152
- if (!canStartDrag || (!isDraggingDown && translateY.value <= 0)) {
153
- return;
154
- }
155
- const isScrollableActive =
156
- hasActive && active !== undefined && active.isGestureActive.value;
157
- isDraggingSheet.set(true);
158
- isDraggingFromScrollable.set(isScrollableActive && activeOffset <= 0);
159
- dragStartTranslateY.set(translateY.value - event.translationY);
160
- isScrollableLocked.set(hasActive);
161
- if (hasActive && active !== undefined && activeOffset <= 0) {
162
- scrollTo(active.ref, 0, 0, false);
163
- }
164
- }
165
- const rawTranslate = dragStartTranslateY.value + event.translationY;
166
- const resolvedDetents = detentsValue.value;
167
- const draggable = isDraggableValue.value;
168
- let maxDraggableTranslateY = sheetHeight.value;
169
- let minDraggableTranslateY = 0;
170
- let foundDraggable = false;
171
- for (let i = 0; i < resolvedDetents.length; i++) {
172
- if (!(draggable[i] ?? true)) continue;
173
- const t = sheetHeight.value - (resolvedDetents[i] ?? 0);
174
- if (!foundDraggable) {
175
- maxDraggableTranslateY = t;
176
- minDraggableTranslateY = t;
177
- foundDraggable = true;
178
- } else {
179
- if (t > maxDraggableTranslateY) maxDraggableTranslateY = t;
180
- if (t < minDraggableTranslateY) minDraggableTranslateY = t;
181
- }
182
- }
183
- const nextTranslate = Math.min(
184
- Math.max(rawTranslate, minDraggableTranslateY),
185
- maxDraggableTranslateY
186
- );
187
- translateY.set(nextTranslate);
188
- if (isDraggingSheet.value && rawTranslate < 0 && hasActive) {
189
- isDraggingSheet.set(false);
190
- isScrollableLocked.set(false);
191
- let targetSnapIndex = -1;
192
- let targetSnapValue = -1;
193
- for (let i = resolvedDetents.length - 1; i >= 0; i--) {
194
- const detentValue = resolvedDetents[i];
195
- if (
196
- detentValue !== undefined &&
197
- (draggable[i] ?? true) &&
198
- detentValue > targetSnapValue
199
- ) {
200
- targetSnapValue = detentValue;
201
- targetSnapIndex = i;
202
- }
203
- }
204
- if (targetSnapIndex === -1) {
205
- const maxSnap = sheetHeight.value;
206
- for (let i = resolvedDetents.length - 1; i >= 0; i--) {
207
- if (resolvedDetents[i] === maxSnap) {
208
- targetSnapIndex = i;
209
- break;
210
- }
211
- }
212
- }
213
- if (targetSnapIndex !== -1) {
214
- if (targetSnapIndex !== currentIndex.value) {
215
- scheduleOnRN(handleIndexChange, targetSnapIndex);
216
- }
217
- animateToIndex(targetSnapIndex);
218
- }
219
- }
220
- })
221
- .onEnd((event) => {
222
- 'worklet';
223
- const wasDragging = isDraggingSheet.value;
224
- isScrollableLocked.set(false);
225
- isDraggingSheet.set(false);
226
- animationTarget.set(NaN);
227
- if (!wasDragging) {
228
- animateToIndex(currentIndex.value);
229
- return;
230
- }
231
- const maxSnap = sheetHeight.value;
232
- const draggable = isDraggableValue.value;
233
- const allPositions = detentsValue.value.map((detentValue, snapIndex) => ({
234
- index: snapIndex,
235
- translateY: maxSnap - detentValue,
236
- isDraggable: draggable[snapIndex] ?? true,
237
- }));
238
- const targetIndex = findSnapTarget(
239
- translateY.value,
240
- event.velocityY,
241
- currentIndex.value,
242
- allPositions
243
- );
244
- const hasIndexChanged = targetIndex !== currentIndex.value;
245
- if (hasIndexChanged) scheduleOnRN(handleIndexChange, targetIndex);
246
- const shouldApplyVelocity =
247
- hasIndexChanged && Number.isFinite(event.velocityY);
248
- animateToIndex(
249
- targetIndex,
250
- shouldApplyVelocity ? event.velocityY : undefined
251
- );
252
- });
253
- };