@swmansion/react-native-bottom-sheet 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,YAAY,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,YAAY,EACV,0BAA0B,EAC1B,wBAAwB,GACzB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,YAAY,EACV,4BAA4B,EAC5B,0BAA0B,GAC3B,MAAM,yBAAyB,CAAC;AACjC,YAAY,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,YAAY,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,YAAY,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,YAAY,EACV,0BAA0B,EAC1B,wBAAwB,GACzB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,YAAY,EACV,4BAA4B,EAC5B,0BAA0B,GAC3B,MAAM,yBAAyB,CAAC;AACjC,YAAY,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC"}
@@ -0,0 +1,20 @@
1
+ import type { PanGesture } from 'react-native-gesture-handler';
2
+ import { type AnimatedRef, type SharedValue } from 'react-native-reanimated';
3
+ interface BottomSheetPanGestureParams {
4
+ animationTarget: SharedValue<number>;
5
+ translateY: SharedValue<number>;
6
+ sheetHeight: SharedValue<number>;
7
+ detentsValue: SharedValue<number[]>;
8
+ isDraggableValue: SharedValue<boolean[]>;
9
+ currentIndex: SharedValue<number>;
10
+ scrollOffset: SharedValue<number>;
11
+ hasScrollable: SharedValue<boolean>;
12
+ isScrollableGestureActive: SharedValue<boolean>;
13
+ isScrollableLocked: SharedValue<boolean>;
14
+ scrollableRef: AnimatedRef<any>;
15
+ handleIndexChange: (nextIndex: number) => void;
16
+ animateToIndex: (targetIndex: number, velocity?: number) => void;
17
+ }
18
+ export declare const useBottomSheetPanGesture: ({ animationTarget, translateY, sheetHeight, detentsValue, isDraggableValue, currentIndex, scrollOffset, hasScrollable, isScrollableGestureActive, isScrollableLocked, scrollableRef, handleIndexChange, animateToIndex, }: BottomSheetPanGestureParams) => PanGesture;
19
+ export {};
20
+ //# sourceMappingURL=useBottomSheetPanGesture.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useBottomSheetPanGesture.d.ts","sourceRoot":"","sources":["../../../src/useBottomSheetPanGesture.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAG/D,OAAO,EAGL,KAAK,WAAW,EAChB,KAAK,WAAW,EAEjB,MAAM,yBAAyB,CAAC;AAIjC,UAAU,2BAA2B;IACnC,eAAe,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACrC,UAAU,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAChC,WAAW,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACjC,YAAY,EAAE,WAAW,CAAC,MAAM,EAAE,CAAC,CAAC;IACpC,gBAAgB,EAAE,WAAW,CAAC,OAAO,EAAE,CAAC,CAAC;IACzC,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAClC,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAClC,aAAa,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IACpC,yBAAyB,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IAChD,kBAAkB,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IACzC,aAAa,EAAE,WAAW,CAAC,GAAG,CAAC,CAAC;IAChC,iBAAiB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/C,cAAc,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CAClE;AAED,eAAO,MAAM,wBAAwB,GAAI,2NActC,2BAA2B,KAAG,UAoKhC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swmansion/react-native-bottom-sheet",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Provides bottom-sheet components for React Native.",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -4,8 +4,6 @@ import type { LayoutChangeEvent } from 'react-native';
4
4
  import { Pressable, StyleSheet, View, useWindowDimensions } from 'react-native';
5
5
  import type { SharedValue, WithSpringConfig } from 'react-native-reanimated';
6
6
  import Animated, {
7
- measure,
8
- scrollTo,
9
7
  useAnimatedRef,
10
8
  useAnimatedReaction,
11
9
  useAnimatedStyle,
@@ -13,13 +11,20 @@ import Animated, {
13
11
  useSharedValue,
14
12
  withSpring,
15
13
  } from 'react-native-reanimated';
16
- import { scheduleOnRN, scheduleOnUI } from 'react-native-worklets';
17
- import { Gesture, GestureDetector } from 'react-native-gesture-handler';
14
+ import { scheduleOnUI } from 'react-native-worklets';
15
+ import { GestureDetector } from 'react-native-gesture-handler';
18
16
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
19
17
  import { Portal } from './BottomSheetProvider';
20
18
  import { BottomSheetContextProvider } from './BottomSheetContext';
21
-
22
- export type Detent = number | 'max';
19
+ import {
20
+ clampIndex,
21
+ isDetentProgrammatic,
22
+ resolveDetent,
23
+ } from './bottomSheetUtils';
24
+ import type { Detent } from './bottomSheetUtils';
25
+ import { useBottomSheetPanGesture } from './useBottomSheetPanGesture';
26
+ export type { Detent, DetentValue } from './bottomSheetUtils';
27
+ export { programmatic } from './bottomSheetUtils';
23
28
 
24
29
  export interface BottomSheetCommonProps {
25
30
  children: ReactNode;
@@ -48,25 +53,6 @@ const DEFAULT_CLOSE_ANIMATION_CONFIG: WithSpringConfig = {
48
53
  overshootClamping: true,
49
54
  };
50
55
 
51
- const VELOCITY_THRESHOLD = 800;
52
-
53
- const resolveDetent = (
54
- detent: Detent,
55
- contentHeight: number,
56
- maxHeight: number
57
- ) => {
58
- if (typeof detent === 'number') return detent;
59
- if (detent === 'max') {
60
- return contentHeight > 0 ? Math.min(contentHeight, maxHeight) : maxHeight;
61
- }
62
- throw new Error(`Invalid detent: \`${detent}\`.`);
63
- };
64
-
65
- const clampIndex = (index: number, detentCount: number) => {
66
- if (detentCount <= 0) return 0;
67
- return Math.min(Math.max(index, 0), detentCount - 1);
68
- };
69
-
70
56
  const DefaultScrim = ({ progress }: { progress: SharedValue<number> }) => {
71
57
  const style = useAnimatedStyle(() => ({ opacity: progress.value }));
72
58
  return (
@@ -96,14 +82,18 @@ export const BottomSheetBase = ({
96
82
  const maxHeight = screenHeight - insets.top;
97
83
  const resolvedIndex = clampIndex(index, detents.length);
98
84
  const [contentHeight, setContentHeight] = useState(0);
85
+
99
86
  if (detents.length === 0) {
100
87
  throw new Error('detents must include at least one value.');
101
88
  }
102
- const normalizedDetents = detents.map((point) => {
103
- const resolved = resolveDetent(point, contentHeight, maxHeight);
89
+
90
+ const normalizedDetents = detents.map((detent) => {
91
+ const resolved = resolveDetent(detent, contentHeight, maxHeight);
104
92
  return Math.max(0, Math.min(resolved, maxHeight));
105
93
  });
94
+ const isDraggable = detents.map((detent) => !isDetentProgrammatic(detent));
106
95
  const initialMaxSnap = Math.max(0, ...normalizedDetents);
96
+
107
97
  const translateY = useSharedValue(initialMaxSnap);
108
98
  const animationTarget = useSharedValue(NaN);
109
99
  const sheetHeight = useSharedValue(initialMaxSnap);
@@ -112,41 +102,50 @@ export const BottomSheetBase = ({
112
102
  const isScrollableGestureActive = useSharedValue(false);
113
103
  const isScrollableLocked = useSharedValue(false);
114
104
  const scrollableRef = useAnimatedRef();
115
- const isDraggingSheet = useSharedValue(false);
116
- const isDraggingFromScrollable = useSharedValue(false);
117
- const panStartY = useSharedValue(0);
118
- const panActivated = useSharedValue(false);
119
- const dragStartTranslateY = useSharedValue(0);
120
- const isTouchWithinScrollable = useSharedValue(false);
105
+
121
106
  const detentsValue = useSharedValue(normalizedDetents);
107
+ const isDraggableValue = useSharedValue(isDraggable);
122
108
  const firstNonzeroDetent = useSharedValue(
123
- normalizedDetents.find((d) => d > 0) ?? 0
109
+ normalizedDetents.find((detent) => detent > 0) ?? 0
124
110
  );
125
111
  const currentIndex = useSharedValue(resolvedIndex);
126
112
  const internalPosition = useDerivedValue(() =>
127
113
  Math.max(0, sheetHeight.value - translateY.value)
128
114
  );
115
+
129
116
  useAnimatedReaction(
130
117
  () => internalPosition.value,
131
118
  (value) => {
132
119
  if (externalPosition !== undefined) externalPosition.set(value);
133
120
  }
134
121
  );
122
+
135
123
  const scrimProgress = useDerivedValue(() => {
136
124
  const target = firstNonzeroDetent.value;
137
125
  if (target <= 0) return 0;
138
126
  const progress = internalPosition.value / target;
139
127
  return Math.min(1, Math.max(0, progress));
140
128
  });
129
+
141
130
  const handleIndexChange = (nextIndex: number) => {
142
131
  onIndexChange?.(nextIndex);
143
132
  };
133
+
144
134
  useEffect(() => {
145
135
  const maxSnap = Math.max(0, ...normalizedDetents);
146
136
  detentsValue.set(normalizedDetents);
137
+ isDraggableValue.set(isDraggable);
147
138
  sheetHeight.set(maxSnap);
148
- firstNonzeroDetent.set(normalizedDetents.find((d) => d > 0) ?? 0);
149
- }, [normalizedDetents, sheetHeight, detentsValue, firstNonzeroDetent]);
139
+ firstNonzeroDetent.set(normalizedDetents.find((detent) => detent > 0) ?? 0);
140
+ }, [
141
+ normalizedDetents,
142
+ isDraggable,
143
+ sheetHeight,
144
+ detentsValue,
145
+ isDraggableValue,
146
+ firstNonzeroDetent,
147
+ ]);
148
+
150
149
  const animateToIndex = useCallback(
151
150
  (targetIndex: number, velocity?: number) => {
152
151
  'worklet';
@@ -174,154 +173,27 @@ export const BottomSheetBase = ({
174
173
  translateY,
175
174
  ]
176
175
  );
176
+
177
177
  useEffect(() => {
178
178
  scheduleOnUI(animateToIndex, resolvedIndex);
179
179
  }, [animateToIndex, resolvedIndex, normalizedDetents]);
180
- const panGesture = Gesture.Pan()
181
- .manualActivation(true)
182
- .onTouchesDown((event) => {
183
- 'worklet';
184
- panActivated.set(false);
185
- isDraggingSheet.set(false);
186
- isDraggingFromScrollable.set(false);
187
- isScrollableLocked.set(false);
188
- isTouchWithinScrollable.set(false);
189
- const touch = event.changedTouches[0] ?? event.allTouches[0];
190
- if (touch !== undefined) {
191
- panStartY.set(touch.absoluteY);
192
- if (hasScrollable.value) {
193
- const layout = measure(scrollableRef);
194
- if (layout !== null) {
195
- const withinX =
196
- touch.absoluteX >= layout.pageX &&
197
- touch.absoluteX <= layout.pageX + layout.width;
198
- const withinY =
199
- touch.absoluteY >= layout.pageY &&
200
- touch.absoluteY <= layout.pageY + layout.height;
201
- isTouchWithinScrollable.set(withinX && withinY);
202
- }
203
- }
204
- }
205
- })
206
- .onTouchesMove((event, stateManager) => {
207
- 'worklet';
208
- if (panActivated.value) return;
209
- const touch = event.changedTouches[0] ?? event.allTouches[0];
210
- if (!touch) return;
211
- const deltaY = touch.absoluteY - panStartY.value;
212
- if (
213
- hasScrollable.value &&
214
- scrollOffset.value > 0 &&
215
- isTouchWithinScrollable.value
216
- ) {
217
- return;
218
- }
219
- if (deltaY > 0 || translateY.value > 0) {
220
- panActivated.set(true);
221
- stateManager.activate();
222
- }
223
- })
224
- .onBegin(() => {
225
- 'worklet';
226
- animationTarget.set(NaN);
227
- isDraggingSheet.set(false);
228
- isDraggingFromScrollable.set(false);
229
- dragStartTranslateY.set(translateY.value);
230
- })
231
- .onUpdate((event) => {
232
- 'worklet';
233
- if (isDraggingSheet.value) {
234
- if (isDraggingFromScrollable.value) {
235
- scrollTo(scrollableRef, 0, 0, false);
236
- }
237
- } else {
238
- const isDraggingDown = event.translationY > 0;
239
- const canStartDrag =
240
- !hasScrollable.value ||
241
- scrollOffset.value <= 0 ||
242
- !isTouchWithinScrollable.value;
243
- if (!canStartDrag || (!isDraggingDown && translateY.value <= 0)) {
244
- return;
245
- }
246
- const isScrollableActive =
247
- hasScrollable.value && isScrollableGestureActive.value;
248
- isDraggingSheet.set(true);
249
- isDraggingFromScrollable.set(
250
- isScrollableActive && isTouchWithinScrollable.value
251
- );
252
- isScrollableLocked.set(hasScrollable.value);
253
- if (isTouchWithinScrollable.value && hasScrollable.value) {
254
- scrollTo(scrollableRef, 0, 0, false);
255
- }
256
- }
257
- const nextTranslate = Math.min(
258
- Math.max(dragStartTranslateY.value + event.translationY, 0),
259
- sheetHeight.value
260
- );
261
- translateY.set(nextTranslate);
262
- if (
263
- isDraggingSheet.value &&
264
- nextTranslate <= 0 &&
265
- isTouchWithinScrollable.value &&
266
- hasScrollable.value
267
- ) {
268
- isDraggingSheet.set(false);
269
- isScrollableLocked.set(false);
270
- const resolvedDetents = detentsValue.value;
271
- const maxSnap = sheetHeight.value;
272
- for (let i = resolvedDetents.length - 1; i >= 0; i--) {
273
- if (resolvedDetents[i] === maxSnap) {
274
- if (i !== currentIndex.value) scheduleOnRN(handleIndexChange, i);
275
- animateToIndex(i);
276
- break;
277
- }
278
- }
279
- }
280
- })
281
- .onEnd((event) => {
282
- 'worklet';
283
- const wasDragging = isDraggingSheet.value;
284
- isScrollableLocked.set(false);
285
- isDraggingSheet.set(false);
286
- animationTarget.set(NaN);
287
- if (!wasDragging) {
288
- animateToIndex(currentIndex.value);
289
- return;
290
- }
291
- const maxSnap = sheetHeight.value;
292
- const allPositions = detentsValue.value.map((point, snapIndex) => ({
293
- index: snapIndex,
294
- translateY: maxSnap - point,
295
- }));
296
- const currentTranslate = translateY.value;
297
- const velocityY = event.velocityY;
298
- let targetIndex = currentIndex.value;
299
- let minDistance = Infinity;
300
- for (const pos of allPositions) {
301
- const distance = Math.abs(currentTranslate - pos.translateY);
302
- if (distance < minDistance) {
303
- minDistance = distance;
304
- targetIndex = pos.index;
305
- }
306
- }
307
- if (Math.abs(velocityY) > VELOCITY_THRESHOLD) {
308
- if (velocityY > 0) {
309
- const lower = allPositions
310
- .filter((pos) => pos.translateY > currentTranslate + 1)
311
- .sort((a, b) => a.translateY - b.translateY)[0];
312
- if (lower !== undefined) targetIndex = lower.index;
313
- } else {
314
- const upper = allPositions
315
- .filter((pos) => pos.translateY < currentTranslate - 1)
316
- .sort((a, b) => b.translateY - a.translateY)[0];
317
- if (upper !== undefined) targetIndex = upper.index;
318
- }
319
- }
320
- const hasIndexChanged = targetIndex !== currentIndex.value;
321
- if (hasIndexChanged) scheduleOnRN(handleIndexChange, targetIndex);
322
- const shouldApplyVelocity = hasIndexChanged && Number.isFinite(velocityY);
323
- animateToIndex(targetIndex, shouldApplyVelocity ? velocityY : undefined);
324
- });
180
+
181
+ const panGesture = useBottomSheetPanGesture({
182
+ animationTarget,
183
+ translateY,
184
+ sheetHeight,
185
+ detentsValue,
186
+ isDraggableValue,
187
+ currentIndex,
188
+ scrollOffset,
189
+ hasScrollable,
190
+ isScrollableGestureActive,
191
+ isScrollableLocked,
192
+ scrollableRef,
193
+ handleIndexChange,
194
+ animateToIndex,
195
+ });
196
+
325
197
  const handleSentinelLayout = (event: LayoutChangeEvent) => {
326
198
  setContentHeight(event.nativeEvent.layout.y);
327
199
  };
@@ -331,6 +203,7 @@ export const BottomSheetBase = ({
331
203
  handleIndexChange(closedIndex);
332
204
  scheduleOnUI(animateToIndex, closedIndex);
333
205
  };
206
+
334
207
  const wrapperStyle = useAnimatedStyle(() => ({
335
208
  transform: [{ translateY: translateY.value }],
336
209
  height: sheetHeight.value,
@@ -344,6 +217,7 @@ export const BottomSheetBase = ({
344
217
  } else if (modal) {
345
218
  scrimElement = <DefaultScrim progress={scrimProgress} />;
346
219
  }
220
+
347
221
  const sheetContent = (
348
222
  <BottomSheetContextProvider
349
223
  value={{
@@ -380,6 +254,7 @@ export const BottomSheetBase = ({
380
254
  </Animated.View>
381
255
  </BottomSheetContextProvider>
382
256
  );
257
+
383
258
  const sheetContainer = (
384
259
  <Animated.View
385
260
  style={StyleSheet.absoluteFill}
@@ -394,5 +269,6 @@ export const BottomSheetBase = ({
394
269
  </Animated.View>
395
270
  );
396
271
  if (modal) return <Portal>{sheetContainer}</Portal>;
272
+
397
273
  return sheetContainer;
398
274
  };
@@ -24,6 +24,7 @@ const PortalHost = () => {
24
24
  useEffect(() => {
25
25
  return context.subscribe(forceRender);
26
26
  }, [context]);
27
+
27
28
  return Array.from(context.getPortals().entries()).map(([key, element]) => (
28
29
  <View key={key} style={StyleSheet.absoluteFill} pointerEvents="box-none">
29
30
  {element}
@@ -56,6 +57,7 @@ export const BottomSheetProvider = ({ children }: { children: ReactNode }) => {
56
57
  getPortals: () => portals,
57
58
  };
58
59
  });
60
+
59
61
  return (
60
62
  <PortalContext.Provider value={context}>
61
63
  {children}
@@ -69,8 +71,10 @@ export const Portal = ({ children }: { children: ReactNode }) => {
69
71
  if (context === null) {
70
72
  throw new Error('`Portal` must be used within `BottomSheetProvider`.');
71
73
  }
74
+
72
75
  const { addPortal, removePortal } = context;
73
76
  const id = useId();
77
+
74
78
  useEffect(() => {
75
79
  addPortal(id, children);
76
80
  }, [id, children, addPortal]);
@@ -0,0 +1,82 @@
1
+ export type DetentValue = number | 'max';
2
+
3
+ export type Detent =
4
+ | DetentValue
5
+ | { value: DetentValue; programmatic?: boolean };
6
+
7
+ export const programmatic = (value: DetentValue): Detent => ({
8
+ value,
9
+ programmatic: true,
10
+ });
11
+
12
+ export const detentValue = (detent: Detent): DetentValue => {
13
+ if (typeof detent === 'object' && detent !== null) return detent.value;
14
+ return detent;
15
+ };
16
+
17
+ export const isDetentProgrammatic = (detent: Detent): boolean => {
18
+ if (typeof detent === 'object' && detent !== null) {
19
+ return detent.programmatic === true;
20
+ }
21
+ return false;
22
+ };
23
+
24
+ const VELOCITY_THRESHOLD = 800;
25
+
26
+ export const findSnapTarget = (
27
+ currentTranslate: number,
28
+ velocityY: number,
29
+ currentIndex: number,
30
+ allPositions: { index: number; translateY: number; isDraggable: boolean }[]
31
+ ) => {
32
+ 'worklet';
33
+ const draggablePositions = allPositions.filter(
34
+ (position) => position.isDraggable
35
+ );
36
+ const effectivePositions =
37
+ draggablePositions.length > 0 ? draggablePositions : allPositions;
38
+
39
+ let targetIndex = currentIndex;
40
+ let minDistance = Infinity;
41
+
42
+ for (const position of effectivePositions) {
43
+ const distance = Math.abs(currentTranslate - position.translateY);
44
+ if (distance < minDistance) {
45
+ minDistance = distance;
46
+ targetIndex = position.index;
47
+ }
48
+ }
49
+
50
+ if (Math.abs(velocityY) > VELOCITY_THRESHOLD) {
51
+ if (velocityY > 0) {
52
+ const lowerPosition = effectivePositions
53
+ .filter((position) => position.translateY > currentTranslate + 1)
54
+ .sort((a, b) => a.translateY - b.translateY)[0];
55
+ if (lowerPosition !== undefined) targetIndex = lowerPosition.index;
56
+ } else {
57
+ const upperPosition = effectivePositions
58
+ .filter((position) => position.translateY < currentTranslate - 1)
59
+ .sort((a, b) => b.translateY - a.translateY)[0];
60
+ if (upperPosition !== undefined) targetIndex = upperPosition.index;
61
+ }
62
+ }
63
+ return targetIndex;
64
+ };
65
+
66
+ export const resolveDetent = (
67
+ detent: Detent,
68
+ contentHeight: number,
69
+ maxHeight: number
70
+ ) => {
71
+ const detentValueInput = detentValue(detent);
72
+ if (typeof detentValueInput === 'number') return detentValueInput;
73
+ if (detentValueInput === 'max') {
74
+ return contentHeight > 0 ? Math.min(contentHeight, maxHeight) : maxHeight;
75
+ }
76
+ throw new Error(`Invalid detent: \`${detentValueInput}\`.`);
77
+ };
78
+
79
+ export const clampIndex = (index: number, detentCount: number) => {
80
+ if (detentCount <= 0) return 0;
81
+ return Math.min(Math.max(index, 0), detentCount - 1);
82
+ };
package/src/index.tsx CHANGED
@@ -13,4 +13,5 @@ export type {
13
13
  BottomSheetScrollViewMethods,
14
14
  BottomSheetScrollViewProps,
15
15
  } from './BottomSheetScrollView';
16
- export type { Detent } from './BottomSheetBase';
16
+ export type { Detent, DetentValue } from './BottomSheetBase';
17
+ export { programmatic } from './BottomSheetBase';