@swmansion/react-native-bottom-sheet 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.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -0
  3. package/lib/module/BottomSheet.js +8 -0
  4. package/lib/module/BottomSheet.js.map +1 -0
  5. package/lib/module/BottomSheetBase.js +309 -0
  6. package/lib/module/BottomSheetBase.js.map +1 -0
  7. package/lib/module/BottomSheetContext.js +13 -0
  8. package/lib/module/BottomSheetContext.js.map +1 -0
  9. package/lib/module/BottomSheetFlatList.js +29 -0
  10. package/lib/module/BottomSheetFlatList.js.map +1 -0
  11. package/lib/module/BottomSheetProvider.js +73 -0
  12. package/lib/module/BottomSheetProvider.js.map +1 -0
  13. package/lib/module/BottomSheetScrollView.js +29 -0
  14. package/lib/module/BottomSheetScrollView.js.map +1 -0
  15. package/lib/module/ModalBottomSheet.js +13 -0
  16. package/lib/module/ModalBottomSheet.js.map +1 -0
  17. package/lib/module/index.js +8 -0
  18. package/lib/module/index.js.map +1 -0
  19. package/lib/module/package.json +1 -0
  20. package/lib/module/useBottomSheetScrollable.js +56 -0
  21. package/lib/module/useBottomSheetScrollable.js.map +1 -0
  22. package/lib/typescript/package.json +1 -0
  23. package/lib/typescript/src/BottomSheet.d.ts +4 -0
  24. package/lib/typescript/src/BottomSheet.d.ts.map +1 -0
  25. package/lib/typescript/src/BottomSheetBase.d.ts +18 -0
  26. package/lib/typescript/src/BottomSheetBase.d.ts.map +1 -0
  27. package/lib/typescript/src/BottomSheetContext.d.ts +17 -0
  28. package/lib/typescript/src/BottomSheetContext.d.ts.map +1 -0
  29. package/lib/typescript/src/BottomSheetFlatList.d.ts +3 -0
  30. package/lib/typescript/src/BottomSheetFlatList.d.ts.map +1 -0
  31. package/lib/typescript/src/BottomSheetProvider.d.ts +8 -0
  32. package/lib/typescript/src/BottomSheetProvider.d.ts.map +1 -0
  33. package/lib/typescript/src/BottomSheetScrollView.d.ts +3 -0
  34. package/lib/typescript/src/BottomSheetScrollView.d.ts.map +1 -0
  35. package/lib/typescript/src/ModalBottomSheet.d.ts +8 -0
  36. package/lib/typescript/src/ModalBottomSheet.d.ts.map +1 -0
  37. package/lib/typescript/src/index.d.ts +9 -0
  38. package/lib/typescript/src/index.d.ts.map +1 -0
  39. package/lib/typescript/src/useBottomSheetScrollable.d.ts +10 -0
  40. package/lib/typescript/src/useBottomSheetScrollable.d.ts.map +1 -0
  41. package/package.json +117 -0
  42. package/src/BottomSheet.tsx +8 -0
  43. package/src/BottomSheetBase.tsx +378 -0
  44. package/src/BottomSheetContext.tsx +30 -0
  45. package/src/BottomSheetFlatList.tsx +24 -0
  46. package/src/BottomSheetProvider.tsx +83 -0
  47. package/src/BottomSheetScrollView.tsx +24 -0
  48. package/src/ModalBottomSheet.tsx +16 -0
  49. package/src/index.tsx +8 -0
  50. package/src/useBottomSheetScrollable.ts +62 -0
@@ -0,0 +1,4 @@
1
+ import type { BottomSheetCommonProps } from './BottomSheetBase';
2
+ export type BottomSheetProps = BottomSheetCommonProps;
3
+ export declare const BottomSheet: (props: BottomSheetProps) => import("react/jsx-runtime").JSX.Element;
4
+ //# sourceMappingURL=BottomSheet.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BottomSheet.d.ts","sourceRoot":"","sources":["../../../src/BottomSheet.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAGhE,MAAM,MAAM,gBAAgB,GAAG,sBAAsB,CAAC;AAEtD,eAAO,MAAM,WAAW,GAAI,OAAO,gBAAgB,4CAElD,CAAC"}
@@ -0,0 +1,18 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { SharedValue, WithSpringConfig } from 'react-native-reanimated';
3
+ export type Detent = number | 'max';
4
+ export interface BottomSheetCommonProps {
5
+ children: ReactNode;
6
+ detents?: Detent[];
7
+ index: number;
8
+ onIndexChange?: (index: number) => void;
9
+ position?: SharedValue<number>;
10
+ openAnimationConfig?: WithSpringConfig;
11
+ closeAnimationConfig?: WithSpringConfig;
12
+ }
13
+ export interface BottomSheetBaseProps extends BottomSheetCommonProps {
14
+ modal?: boolean;
15
+ renderScrim?: (progress: SharedValue<number>) => ReactNode;
16
+ }
17
+ export declare const BottomSheetBase: ({ children, detents, index, onIndexChange, position: externalPosition, openAnimationConfig, closeAnimationConfig, modal, renderScrim, }: BottomSheetBaseProps) => import("react/jsx-runtime").JSX.Element;
18
+ //# sourceMappingURL=BottomSheetBase.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BottomSheetBase.d.ts","sourceRoot":"","sources":["../../../src/BottomSheetBase.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAGvC,OAAO,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAiB7E,MAAM,MAAM,MAAM,GAAG,MAAM,GAAG,KAAK,CAAC;AAEpC,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,SAAS,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,QAAQ,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC/B,mBAAmB,CAAC,EAAE,gBAAgB,CAAC;IACvC,oBAAoB,CAAC,EAAE,gBAAgB,CAAC;CACzC;AAED,MAAM,WAAW,oBAAqB,SAAQ,sBAAsB;IAClE,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,KAAK,SAAS,CAAC;CAC5D;AA8CD,eAAO,MAAM,eAAe,GAAI,yIAU7B,oBAAoB,4CA6RtB,CAAC"}
@@ -0,0 +1,17 @@
1
+ import type { PanGesture } from 'react-native-gesture-handler';
2
+ import type { AnimatedRef, SharedValue } from 'react-native-reanimated';
3
+ export interface BottomSheetContextType {
4
+ translateY: SharedValue<number>;
5
+ position: SharedValue<number>;
6
+ index: SharedValue<number>;
7
+ sheetHeight: SharedValue<number>;
8
+ scrollOffset: SharedValue<number>;
9
+ scrollableRef: AnimatedRef<any>;
10
+ hasScrollable: SharedValue<boolean>;
11
+ isScrollableGestureActive: SharedValue<boolean>;
12
+ isScrollableLocked: SharedValue<boolean>;
13
+ panGesture: PanGesture;
14
+ }
15
+ export declare const BottomSheetContextProvider: import("react").Provider<BottomSheetContextType | null>;
16
+ export declare const useBottomSheetContext: () => BottomSheetContextType;
17
+ //# sourceMappingURL=BottomSheetContext.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BottomSheetContext.d.ts","sourceRoot":"","sources":["../../../src/BottomSheetContext.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAC/D,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAExE,MAAM,WAAW,sBAAsB;IACrC,UAAU,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAChC,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC9B,KAAK,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC3B,WAAW,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACjC,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAClC,aAAa,EAAE,WAAW,CAAC,GAAG,CAAC,CAAC;IAChC,aAAa,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IACpC,yBAAyB,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IAChD,kBAAkB,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IACzC,UAAU,EAAE,UAAU,CAAC;CACxB;AAID,eAAO,MAAM,0BAA0B,yDAA8B,CAAC;AAEtE,eAAO,MAAM,qBAAqB,8BAQjC,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { FlatListPropsWithLayout } from 'react-native-reanimated';
2
+ export declare const BottomSheetFlatList: <T>(props: Omit<FlatListPropsWithLayout<T>, "onScroll">) => import("react/jsx-runtime").JSX.Element;
3
+ //# sourceMappingURL=BottomSheetFlatList.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BottomSheetFlatList.d.ts","sourceRoot":"","sources":["../../../src/BottomSheetFlatList.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAKvE,eAAO,MAAM,mBAAmB,GAAI,CAAC,EACnC,OAAO,IAAI,CAAC,uBAAuB,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC,4CAgBpD,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { ReactNode } from 'react';
2
+ export declare const BottomSheetProvider: ({ children }: {
3
+ children: ReactNode;
4
+ }) => import("react/jsx-runtime").JSX.Element;
5
+ export declare const Portal: ({ children }: {
6
+ children: ReactNode;
7
+ }) => null;
8
+ //# sourceMappingURL=BottomSheetProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BottomSheetProvider.d.ts","sourceRoot":"","sources":["../../../src/BottomSheetProvider.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAyBvC,eAAO,MAAM,mBAAmB,GAAI,cAAc;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,4CA+BxE,CAAC;AAEF,eAAO,MAAM,MAAM,GAAI,cAAc;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,SAgB3D,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { ScrollViewProps } from 'react-native';
2
+ export declare const BottomSheetScrollView: (props: Omit<ScrollViewProps, "onScroll">) => import("react/jsx-runtime").JSX.Element;
3
+ //# sourceMappingURL=BottomSheetScrollView.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BottomSheetScrollView.d.ts","sourceRoot":"","sources":["../../../src/BottomSheetScrollView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAMpD,eAAO,MAAM,qBAAqB,GAChC,OAAO,IAAI,CAAC,eAAe,EAAE,UAAU,CAAC,4CAgBzC,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { SharedValue } from 'react-native-reanimated';
3
+ import type { BottomSheetCommonProps } from './BottomSheetBase';
4
+ export interface ModalBottomSheetProps extends BottomSheetCommonProps {
5
+ scrim?: (progress: SharedValue<number>) => ReactNode;
6
+ }
7
+ export declare const ModalBottomSheet: ({ scrim, ...props }: ModalBottomSheetProps) => import("react/jsx-runtime").JSX.Element;
8
+ //# sourceMappingURL=ModalBottomSheet.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ModalBottomSheet.d.ts","sourceRoot":"","sources":["../../../src/ModalBottomSheet.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE3D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAGhE,MAAM,WAAW,qBAAsB,SAAQ,sBAAsB;IACnE,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,KAAK,SAAS,CAAC;CACtD;AAED,eAAO,MAAM,gBAAgB,GAAI,qBAG9B,qBAAqB,4CAEvB,CAAC"}
@@ -0,0 +1,9 @@
1
+ export { BottomSheet } from './BottomSheet';
2
+ export type { BottomSheetProps } from './BottomSheet';
3
+ export { ModalBottomSheet } from './ModalBottomSheet';
4
+ export type { ModalBottomSheetProps } from './ModalBottomSheet';
5
+ export { BottomSheetProvider } from './BottomSheetProvider';
6
+ export { BottomSheetFlatList } from './BottomSheetFlatList';
7
+ export { BottomSheetScrollView } from './BottomSheetScrollView';
8
+ export type { Detent } from './BottomSheetBase';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,YAAY,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC"}
@@ -0,0 +1,10 @@
1
+ import { type SharedValue } from 'react-native-reanimated';
2
+ export declare const useBottomSheetScrollable: (baseScrollEnabled?: boolean | SharedValue<boolean | undefined>) => {
3
+ scrollHandler: import("react-native-reanimated").ScrollHandlerProcessed<Record<string, unknown>>;
4
+ scrollableRef: import("react-native-reanimated").AnimatedRef<any>;
5
+ nativeGesture: import("react-native-gesture-handler/lib/typescript/handlers/gestures/nativeGesture").NativeGesture;
6
+ animatedProps: Partial<{
7
+ scrollEnabled: boolean;
8
+ }>;
9
+ };
10
+ //# sourceMappingURL=useBottomSheetScrollable.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useBottomSheetScrollable.d.ts","sourceRoot":"","sources":["../../../src/useBottomSheetScrollable.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,WAAW,EAGjB,MAAM,yBAAyB,CAAC;AAIjC,eAAO,MAAM,wBAAwB,GACnC,oBAAmB,OAAO,GAAG,WAAW,CAAC,OAAO,GAAG,SAAS,CAAQ;;;;;;;CAkDrE,CAAC"}
package/package.json ADDED
@@ -0,0 +1,117 @@
1
+ {
2
+ "name": "@swmansion/react-native-bottom-sheet",
3
+ "version": "0.1.0",
4
+ "description": "Provides bottom-sheet components for React Native.",
5
+ "main": "./lib/module/index.js",
6
+ "types": "./lib/typescript/src/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "source": "./src/index.tsx",
10
+ "types": "./lib/typescript/src/index.d.ts",
11
+ "default": "./lib/module/index.js"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
15
+ "files": [
16
+ "src",
17
+ "lib",
18
+ "android",
19
+ "ios",
20
+ "cpp",
21
+ "*.podspec",
22
+ "react-native.config.js",
23
+ "!ios/build",
24
+ "!android/build",
25
+ "!android/gradle",
26
+ "!android/gradlew",
27
+ "!android/gradlew.bat",
28
+ "!android/local.properties",
29
+ "!**/__tests__",
30
+ "!**/__fixtures__",
31
+ "!**/__mocks__",
32
+ "!**/.*"
33
+ ],
34
+ "scripts": {
35
+ "example": "yarn workspace @swmansion/react-native-bottom-sheet-example",
36
+ "clean": "del-cli lib",
37
+ "prepare": "bob build",
38
+ "typecheck": "tsc",
39
+ "lint": "eslint \"**/*.{js,ts,tsx}\""
40
+ },
41
+ "keywords": [
42
+ "react-native",
43
+ "android",
44
+ "ios"
45
+ ],
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/software-mansion-labs/react-native-bottom-sheet.git"
49
+ },
50
+ "author": "Software Mansion",
51
+ "license": "MIT",
52
+ "bugs": {
53
+ "url": "https://github.com/software-mansion-labs/react-native-bottom-sheet/issues"
54
+ },
55
+ "homepage": "https://github.com/software-mansion-labs/react-native-bottom-sheet",
56
+ "publishConfig": {
57
+ "registry": "https://registry.npmjs.org"
58
+ },
59
+ "devDependencies": {
60
+ "@eslint/compat": "1.4.1",
61
+ "@eslint/eslintrc": "3.3.3",
62
+ "@eslint/js": "9.39.2",
63
+ "@react-native/babel-preset": "0.83.0",
64
+ "@react-native/eslint-config": "0.83.0",
65
+ "@types/react": "19.2.14",
66
+ "del-cli": "6.0.0",
67
+ "eslint": "9.39.2",
68
+ "eslint-config-prettier": "10.1.8",
69
+ "eslint-plugin-prettier": "5.5.5",
70
+ "prettier": "2.8.8",
71
+ "react": "19.1.0",
72
+ "react-native": "0.81.5",
73
+ "react-native-builder-bob": "0.40.18",
74
+ "react-native-gesture-handler": "2.28.0",
75
+ "react-native-reanimated": "4.1.6",
76
+ "react-native-safe-area-context": "5.6.2",
77
+ "react-native-worklets": "0.5.1",
78
+ "typescript": "5.9.3"
79
+ },
80
+ "peerDependencies": {
81
+ "react": ">=18.0.0",
82
+ "react-native": ">=0.76.0",
83
+ "react-native-gesture-handler": ">=2.14.0",
84
+ "react-native-reanimated": ">=3.16.0",
85
+ "react-native-safe-area-context": ">=4.0.0",
86
+ "react-native-worklets": ">=0.4.0"
87
+ },
88
+ "workspaces": [
89
+ "example"
90
+ ],
91
+ "packageManager": "yarn@4.11.0",
92
+ "react-native-builder-bob": {
93
+ "source": "src",
94
+ "output": "lib",
95
+ "targets": [
96
+ [
97
+ "module",
98
+ {
99
+ "esm": true
100
+ }
101
+ ],
102
+ [
103
+ "typescript",
104
+ {
105
+ "project": "tsconfig.build.json"
106
+ }
107
+ ]
108
+ ]
109
+ },
110
+ "prettier": {
111
+ "quoteProps": "consistent",
112
+ "singleQuote": true,
113
+ "tabWidth": 2,
114
+ "trailingComma": "es5",
115
+ "useTabs": false
116
+ }
117
+ }
@@ -0,0 +1,8 @@
1
+ import type { BottomSheetCommonProps } from './BottomSheetBase';
2
+ import { BottomSheetBase } from './BottomSheetBase';
3
+
4
+ export type BottomSheetProps = BottomSheetCommonProps;
5
+
6
+ export const BottomSheet = (props: BottomSheetProps) => (
7
+ <BottomSheetBase {...props} />
8
+ );
@@ -0,0 +1,378 @@
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
+ measure,
8
+ scrollTo,
9
+ useAnimatedRef,
10
+ useAnimatedReaction,
11
+ useAnimatedStyle,
12
+ useDerivedValue,
13
+ useSharedValue,
14
+ withSpring,
15
+ } from 'react-native-reanimated';
16
+ import { scheduleOnRN, scheduleOnUI } from 'react-native-worklets';
17
+ import { Gesture, GestureDetector } from 'react-native-gesture-handler';
18
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
19
+ import { Portal } from './BottomSheetProvider';
20
+ import { BottomSheetContextProvider } from './BottomSheetContext';
21
+
22
+ export type Detent = number | 'max';
23
+
24
+ export interface BottomSheetCommonProps {
25
+ children: ReactNode;
26
+ detents?: Detent[];
27
+ index: number;
28
+ onIndexChange?: (index: number) => void;
29
+ position?: SharedValue<number>;
30
+ openAnimationConfig?: WithSpringConfig;
31
+ closeAnimationConfig?: WithSpringConfig;
32
+ }
33
+
34
+ export interface BottomSheetBaseProps extends BottomSheetCommonProps {
35
+ modal?: boolean;
36
+ renderScrim?: (progress: SharedValue<number>) => ReactNode;
37
+ }
38
+
39
+ const DEFAULT_OPEN_ANIMATION_CONFIG: WithSpringConfig = {
40
+ dampingRatio: 1,
41
+ duration: 300,
42
+ overshootClamping: true,
43
+ };
44
+
45
+ const DEFAULT_CLOSE_ANIMATION_CONFIG: WithSpringConfig = {
46
+ dampingRatio: 1,
47
+ duration: 250,
48
+ overshootClamping: true,
49
+ };
50
+
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
+ const DefaultScrim = ({ progress }: { progress: SharedValue<number> }) => {
71
+ const style = useAnimatedStyle(() => ({ opacity: progress.value }));
72
+ return (
73
+ <Animated.View
74
+ style={[
75
+ StyleSheet.absoluteFill,
76
+ { flex: 1, backgroundColor: 'rgba(0, 0, 0, 0.5)' },
77
+ style,
78
+ ]}
79
+ />
80
+ );
81
+ };
82
+
83
+ export const BottomSheetBase = ({
84
+ children,
85
+ detents = [0, 'max'],
86
+ index,
87
+ onIndexChange,
88
+ position: externalPosition,
89
+ openAnimationConfig = DEFAULT_OPEN_ANIMATION_CONFIG,
90
+ closeAnimationConfig = DEFAULT_CLOSE_ANIMATION_CONFIG,
91
+ modal = false,
92
+ renderScrim,
93
+ }: BottomSheetBaseProps) => {
94
+ const { height: screenHeight } = useWindowDimensions();
95
+ const insets = useSafeAreaInsets();
96
+ const maxHeight = screenHeight - insets.top;
97
+ const resolvedIndex = clampIndex(index, detents.length);
98
+ const [contentHeight, setContentHeight] = useState(0);
99
+ if (detents.length === 0) {
100
+ throw new Error('detents must include at least one value.');
101
+ }
102
+ const normalizedDetents = detents.map((point) => {
103
+ const resolved = resolveDetent(point, contentHeight, maxHeight);
104
+ return Math.max(0, Math.min(resolved, maxHeight));
105
+ });
106
+ const initialMaxSnap = Math.max(0, ...normalizedDetents);
107
+ const translateY = useSharedValue(initialMaxSnap);
108
+ const animationTarget = useSharedValue(NaN);
109
+ const sheetHeight = useSharedValue(initialMaxSnap);
110
+ const scrollOffset = useSharedValue(0);
111
+ const hasScrollable = useSharedValue(false);
112
+ const isScrollableGestureActive = useSharedValue(false);
113
+ const isScrollableLocked = useSharedValue(false);
114
+ 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);
121
+ const detentsValue = useSharedValue(normalizedDetents);
122
+ const currentIndex = useSharedValue(resolvedIndex);
123
+ const internalPosition = useDerivedValue(() =>
124
+ Math.max(0, sheetHeight.value - translateY.value)
125
+ );
126
+ useAnimatedReaction(
127
+ () => internalPosition.value,
128
+ (value) => {
129
+ if (externalPosition !== undefined) externalPosition.set(value);
130
+ }
131
+ );
132
+ const scrimProgress = useDerivedValue(() => {
133
+ const maxSnap = sheetHeight.value;
134
+ if (maxSnap <= 0) return 0;
135
+ const progress = internalPosition.value / maxSnap;
136
+ return Math.min(1, Math.max(0, progress));
137
+ });
138
+ const handleIndexChange = (nextIndex: number) => {
139
+ onIndexChange?.(nextIndex);
140
+ };
141
+ useEffect(() => {
142
+ const maxSnap = Math.max(0, ...normalizedDetents);
143
+ detentsValue.set(normalizedDetents);
144
+ sheetHeight.set(maxSnap);
145
+ }, [normalizedDetents, sheetHeight, detentsValue]);
146
+ const animateToIndex = useCallback(
147
+ (targetIndex: number, velocity?: number) => {
148
+ 'worklet';
149
+ const maxSnap = sheetHeight.value;
150
+ const targetTranslate = maxSnap - (detentsValue.value[targetIndex] ?? 0);
151
+ if (animationTarget.value === targetTranslate && velocity === undefined) {
152
+ currentIndex.set(targetIndex);
153
+ return;
154
+ }
155
+ animationTarget.set(targetTranslate);
156
+ const isOpening = targetTranslate < translateY.value;
157
+ const baseConfig = isOpening ? openAnimationConfig : closeAnimationConfig;
158
+ const springConfig =
159
+ velocity === undefined ? baseConfig : { ...baseConfig, velocity };
160
+ translateY.set(withSpring(targetTranslate, springConfig));
161
+ currentIndex.set(targetIndex);
162
+ },
163
+ [
164
+ animationTarget,
165
+ closeAnimationConfig,
166
+ currentIndex,
167
+ detentsValue,
168
+ openAnimationConfig,
169
+ sheetHeight,
170
+ translateY,
171
+ ]
172
+ );
173
+ useEffect(() => {
174
+ scheduleOnUI(animateToIndex, resolvedIndex);
175
+ }, [animateToIndex, resolvedIndex, normalizedDetents]);
176
+ const panGesture = Gesture.Pan()
177
+ .manualActivation(true)
178
+ .onTouchesDown((event) => {
179
+ 'worklet';
180
+ panActivated.set(false);
181
+ isDraggingSheet.set(false);
182
+ isDraggingFromScrollable.set(false);
183
+ isScrollableLocked.set(false);
184
+ isTouchWithinScrollable.set(false);
185
+ const touch = event.changedTouches[0] ?? event.allTouches[0];
186
+ if (touch) panStartY.set(touch.absoluteY);
187
+ if (touch && hasScrollable.value) {
188
+ const layout = measure(scrollableRef);
189
+ if (layout) {
190
+ const withinX =
191
+ touch.absoluteX >= layout.pageX &&
192
+ touch.absoluteX <= layout.pageX + layout.width;
193
+ const withinY =
194
+ touch.absoluteY >= layout.pageY &&
195
+ touch.absoluteY <= layout.pageY + layout.height;
196
+ isTouchWithinScrollable.set(withinX && withinY);
197
+ }
198
+ }
199
+ })
200
+ .onTouchesMove((event, stateManager) => {
201
+ 'worklet';
202
+ if (panActivated.value) return;
203
+ const touch = event.changedTouches[0] ?? event.allTouches[0];
204
+ if (!touch) return;
205
+ const deltaY = touch.absoluteY - panStartY.value;
206
+ if (
207
+ hasScrollable.value &&
208
+ scrollOffset.value > 0 &&
209
+ isTouchWithinScrollable.value
210
+ ) {
211
+ return;
212
+ }
213
+ if (deltaY > 0 || translateY.value > 0) {
214
+ panActivated.set(true);
215
+ stateManager.activate();
216
+ }
217
+ })
218
+ .onBegin(() => {
219
+ 'worklet';
220
+ animationTarget.set(NaN);
221
+ isDraggingSheet.set(false);
222
+ isDraggingFromScrollable.set(false);
223
+ dragStartTranslateY.set(translateY.value);
224
+ })
225
+ .onUpdate((event) => {
226
+ 'worklet';
227
+ if (isDraggingSheet.value) {
228
+ if (isDraggingFromScrollable.value) {
229
+ scrollTo(scrollableRef, 0, 0, false);
230
+ }
231
+ const nextTranslate = Math.min(
232
+ Math.max(dragStartTranslateY.value + event.translationY, 0),
233
+ sheetHeight.value
234
+ );
235
+ translateY.set(nextTranslate);
236
+ return;
237
+ }
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
+ const nextTranslate = Math.min(
257
+ Math.max(dragStartTranslateY.value + event.translationY, 0),
258
+ sheetHeight.value
259
+ );
260
+ translateY.set(nextTranslate);
261
+ })
262
+ .onEnd((event) => {
263
+ 'worklet';
264
+ const wasDragging = isDraggingSheet.value;
265
+ isScrollableLocked.set(false);
266
+ isDraggingSheet.set(false);
267
+ if (!wasDragging) {
268
+ animateToIndex(currentIndex.value);
269
+ return;
270
+ }
271
+ const maxSnap = sheetHeight.value;
272
+ const allPositions = detentsValue.value.map((point, snapIndex) => ({
273
+ index: snapIndex,
274
+ translateY: maxSnap - point,
275
+ }));
276
+ const currentTranslate = translateY.value;
277
+ const velocityY = event.velocityY;
278
+ let targetIndex = currentIndex.value;
279
+ let minDistance = Infinity;
280
+ for (const pos of allPositions) {
281
+ const distance = Math.abs(currentTranslate - pos.translateY);
282
+ if (distance < minDistance) {
283
+ minDistance = distance;
284
+ targetIndex = pos.index;
285
+ }
286
+ }
287
+ if (Math.abs(velocityY) > VELOCITY_THRESHOLD) {
288
+ if (velocityY > 0) {
289
+ const lower = allPositions
290
+ .filter((pos) => pos.translateY > currentTranslate + 1)
291
+ .sort((a, b) => a.translateY - b.translateY);
292
+ if (lower.length > 0) targetIndex = lower[0]!.index;
293
+ } else {
294
+ const upper = allPositions
295
+ .filter((pos) => pos.translateY < currentTranslate - 1)
296
+ .sort((a, b) => b.translateY - a.translateY);
297
+ if (upper.length > 0) targetIndex = upper[0]!.index;
298
+ }
299
+ }
300
+ const hasIndexChanged = targetIndex !== currentIndex.value;
301
+ if (hasIndexChanged) scheduleOnRN(handleIndexChange, targetIndex);
302
+ const shouldApplyVelocity = hasIndexChanged && Number.isFinite(velocityY);
303
+ animateToIndex(targetIndex, shouldApplyVelocity ? velocityY : undefined);
304
+ });
305
+ const handleSentinelLayout = (event: LayoutChangeEvent) => {
306
+ setContentHeight(event.nativeEvent.layout.y);
307
+ };
308
+ const closedIndex = normalizedDetents.indexOf(0);
309
+ const handleScrimPress = () => {
310
+ if (closedIndex === -1 || resolvedIndex === closedIndex) return;
311
+ handleIndexChange(closedIndex);
312
+ scheduleOnUI(animateToIndex, closedIndex);
313
+ };
314
+ const wrapperStyle = useAnimatedStyle(() => ({
315
+ transform: [{ translateY: translateY.value }],
316
+ height: sheetHeight.value,
317
+ opacity: translateY.value >= sheetHeight.value ? 0 : 1,
318
+ }));
319
+ const isCollapsed = normalizedDetents[resolvedIndex] === 0;
320
+ const pointerEvents = modal ? (isCollapsed ? 'none' : 'auto') : 'box-none';
321
+ let scrimElement: ReactNode | null = null;
322
+ if (renderScrim) {
323
+ scrimElement = renderScrim(scrimProgress);
324
+ } else if (modal) {
325
+ scrimElement = <DefaultScrim progress={scrimProgress} />;
326
+ }
327
+ const sheetContent = (
328
+ <BottomSheetContextProvider
329
+ value={{
330
+ translateY,
331
+ position: internalPosition,
332
+ index: currentIndex,
333
+ sheetHeight,
334
+ scrollOffset,
335
+ scrollableRef,
336
+ hasScrollable,
337
+ isScrollableGestureActive,
338
+ isScrollableLocked,
339
+ panGesture,
340
+ }}
341
+ >
342
+ <Animated.View
343
+ style={[
344
+ {
345
+ position: 'absolute',
346
+ bottom: 0,
347
+ left: 0,
348
+ right: 0,
349
+ },
350
+ wrapperStyle,
351
+ ]}
352
+ pointerEvents="box-none"
353
+ >
354
+ <GestureDetector gesture={panGesture}>
355
+ <View style={{ flex: 1 }} pointerEvents="box-none">
356
+ {children}
357
+ <View onLayout={handleSentinelLayout} pointerEvents="none" />
358
+ </View>
359
+ </GestureDetector>
360
+ </Animated.View>
361
+ </BottomSheetContextProvider>
362
+ );
363
+ const sheetContainer = (
364
+ <Animated.View
365
+ style={StyleSheet.absoluteFill}
366
+ pointerEvents={pointerEvents}
367
+ >
368
+ {modal && scrimElement ? (
369
+ <Pressable style={StyleSheet.absoluteFill} onPress={handleScrimPress}>
370
+ {scrimElement}
371
+ </Pressable>
372
+ ) : null}
373
+ {sheetContent}
374
+ </Animated.View>
375
+ );
376
+ if (modal) return <Portal>{sheetContainer}</Portal>;
377
+ return sheetContainer;
378
+ };
@@ -0,0 +1,30 @@
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 BottomSheetContextType {
6
+ translateY: SharedValue<number>;
7
+ position: SharedValue<number>;
8
+ index: SharedValue<number>;
9
+ sheetHeight: SharedValue<number>;
10
+ scrollOffset: SharedValue<number>;
11
+ scrollableRef: AnimatedRef<any>;
12
+ hasScrollable: SharedValue<boolean>;
13
+ isScrollableGestureActive: SharedValue<boolean>;
14
+ isScrollableLocked: SharedValue<boolean>;
15
+ panGesture: PanGesture;
16
+ }
17
+
18
+ const BottomSheetContext = createContext<BottomSheetContextType | null>(null);
19
+
20
+ export const BottomSheetContextProvider = BottomSheetContext.Provider;
21
+
22
+ export const useBottomSheetContext = () => {
23
+ const context = useContext(BottomSheetContext);
24
+ if (context === null) {
25
+ throw new Error(
26
+ '`useBottomSheetContext` must be used within `BottomSheet`.'
27
+ );
28
+ }
29
+ return context;
30
+ };