@shaquillehinds/react-native-bottom-sheet 0.0.5 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,27 +1,31 @@
1
1
  # @shaquillehinds/react-native-bottom-sheet
2
2
 
3
- A performant, highly customizable bottom sheet component for React Native that just works. Built with React Native Reanimated and Gesture Handler for smooth 60fps animations and natural gesture interactions.
4
-
5
- [![npm version](https://badge.fury.io/js/@shaquillehinds%2Freact-native-bottom-sheet.svg)](https://badge.fury.io/js/@shaquillehinds%2Freact-native-bottom-sheet)
6
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
-
8
- <img src="https://raw.githubusercontent.com/shaquillehinds/react-native-bottom-sheet/master/assets/bottomsheet.gif" alt="example" height="500"/>
9
-
10
- ## Features
11
-
12
- - 🎯 **Multiple Snap Points** - Define custom snap positions as percentages of screen height
13
- - 📱 **Keyboard Aware** - Intelligent keyboard avoidance with per-input customization
14
- - 🎨 **Fully Customizable** - Style every aspect from bumper to backdrop
15
- - 🔄 **Portal System** - Renders at app root level above navigation with `BottomSheetPortalProvider`
16
- - 🎪 **Advanced Portal APIs** - Manual portal control with `useBottomSheetPortal` and `useBottomSheetPortalComponent`
17
- - 📜 **Scrollable Content** - Built-in FlatList and ScrollView components with proper gesture handlin
18
- - **High Performance** - Optimized animations using `useImperativeHandle` for render isolation
19
- - 🎭 **Modal & Inline Modes** - Use as a full modal or inline component
20
- - 🔒 **Type Safe** - Full TypeScript support with comprehensive type definitions
21
- - 🌐 **Cross Platform** - iOS and Android support with platform-specific optimizations
22
- - 🎬 **Flexible Drag Areas** - Configure draggable regions (full, bumper, or none)
23
- - 💾 **Keep Mounted Option** - Persist component state with partial visibility
24
- - ⏱️ **Content Loading Strategies** - Delay content rendering for improved performance
3
+ A performant, highly customizable bottom sheet for React Native. Built on
4
+ Reanimated and Gesture Handler, with render isolation baked into the mounting
5
+ model so closed sheets cost nothing.
6
+
7
+ ---
8
+
9
+ ## Contents
10
+
11
+ - [Installation](#installation)
12
+ - [Setup](#setup)
13
+ - [Quick start](#quick-start)
14
+ - [The content component pattern](#the-content-component-pattern)
15
+ - [Components](#components)
16
+ - [Controlling the sheet](#controlling-the-sheet)
17
+ - [Snap points](#snap-points)
18
+ - [Scrollable content](#scrollable-content)
19
+ - [Keyboard handling](#keyboard-handling)
20
+ - [Persistent sheets](#persistent-sheets)
21
+ - [Appearance](#appearance)
22
+ - [Portal system](#portal-system)
23
+ - [API reference](#api-reference)
24
+ - [Recipes](#recipes)
25
+ - [Troubleshooting](#troubleshooting)
26
+ - [AI agent rules](#ai-agent-rules)
27
+
28
+ ---
25
29
 
26
30
  ## Installation
27
31
 
@@ -29,23 +33,31 @@ A performant, highly customizable bottom sheet component for React Native that j
29
33
  npm install @shaquillehinds/react-native-bottom-sheet
30
34
  ```
31
35
 
32
- or
36
+ Peer dependencies:
33
37
 
34
38
  ```bash
35
- yarn add @shaquillehinds/react-native-bottom-sheet
39
+ npm install react-native-reanimated react-native-gesture-handler @shaquillehinds/react-native-essentials
36
40
  ```
37
41
 
38
- ### Peer Dependencies
42
+ | Package | Version |
43
+ | ----------------------------------------- | ------- |
44
+ | `react-native-reanimated` | ^3.0.0 |
45
+ | `react-native-gesture-handler` | ^2.0.0 |
46
+ | `@shaquillehinds/react-native-essentials` | ^1.8.0 |
39
47
 
40
- This package requires the following peer dependencies:
48
+ Reanimated's Babel plugin must be last in your plugin list:
41
49
 
42
- ```bash
43
- npm install react-native-reanimated react-native-gesture-handler @shaquillehinds/react-native-essentials
50
+ ```js
51
+ // babel.config.js
52
+ module.exports = {
53
+ presets: ['module:metro-react-native-babel-preset'],
54
+ plugins: ['react-native-reanimated/plugin'],
55
+ };
44
56
  ```
45
57
 
46
- ### Setup
58
+ ## Setup
47
59
 
48
- Wrap your app with `BottomSheetPortalProvider` at the root level:
60
+ Wrap your app root **above** the navigation container:
49
61
 
50
62
  ```tsx
51
63
  import { BottomSheetPortalProvider } from '@shaquillehinds/react-native-bottom-sheet';
@@ -53,333 +65,335 @@ import { BottomSheetPortalProvider } from '@shaquillehinds/react-native-bottom-s
53
65
  export default function App() {
54
66
  return (
55
67
  <BottomSheetPortalProvider>
56
- {/* Your app content */}
68
+ <NavigationContainer>
69
+ <RootNavigator />
70
+ </NavigationContainer>
57
71
  </BottomSheetPortalProvider>
58
72
  );
59
73
  }
60
74
  ```
61
75
 
62
- This provider enables bottom sheets to render at the top level of your app, above all other content including navigation.
63
-
64
- ## Quick Start
76
+ Sheets render into this portal by default, which is what puts them above
77
+ navigation, tab bars and headers.
65
78
 
66
- First, wrap your app with the portal provider:
67
-
68
- ```tsx
69
- // App.tsx
70
- import { BottomSheetPortalProvider } from '@shaquillehinds/react-native-bottom-sheet';
71
-
72
- export default function App() {
73
- return (
74
- <BottomSheetPortalProvider>
75
- <YourApp />
76
- </BottomSheetPortalProvider>
77
- );
78
- }
79
- ```
79
+ ---
80
80
 
81
- ### Basic Modal Bottom Sheet
81
+ ## Quick start
82
82
 
83
83
  ```tsx
84
- import React, { useRef, useState } from 'react';
84
+ import { useState } from 'react';
85
85
  import { View, Text, Button } from 'react-native';
86
- import {
87
- BottomSheetModal,
88
- useBottomSheetRef,
89
- } from '@shaquillehinds/react-native-bottom-sheet';
90
- import type { BottomModalRefObject } from '@shaquillehinds/react-native-bottom-sheet';
86
+ import { BottomSheetModal } from '@shaquillehinds/react-native-bottom-sheet';
91
87
 
92
- export default function MyScreen() {
88
+ export default function Screen() {
93
89
  const [showModal, setShowModal] = useState(false);
94
- const bottomSheetRef = useRef<BottomModalRefObject>(null);
95
90
 
96
91
  return (
97
- <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
98
- <Button title="Open Bottom Sheet" onPress={() => setShowModal(true)} />
92
+ <View style={{ flex: 1 }}>
93
+ <Button title="Open" onPress={() => setShowModal(true)} />
99
94
 
100
95
  <BottomSheetModal
101
- ref={bottomSheetRef}
102
96
  showModal={showModal}
103
97
  setShowModal={setShowModal}
104
- snapPoints={[50, 75, 90]}
98
+ snapPoints={[50, 90]}
105
99
  >
106
- <View style={{ padding: 20 }}>
107
- <Text>Bottom Sheet Content</Text>
108
- </View>
100
+ <SheetContent onDone={() => setShowModal(false)} />
109
101
  </BottomSheetModal>
110
102
  </View>
111
103
  );
112
104
  }
113
105
  ```
114
106
 
115
- ### Inline Bottom Sheet
107
+ Note the children are a component, not inline JSX. That is the pattern the whole
108
+ library is built around — the next section explains why.
116
109
 
117
- ```tsx
118
- import { BottomSheet } from '@shaquillehinds/react-native-bottom-sheet';
110
+ ---
119
111
 
120
- <BottomSheet
121
- showModal={showModal}
122
- setShowModal={setShowModal}
123
- snapPoints={[30, 60, 90]}
124
- keepMounted={true}
125
- bottomOffset={50}
126
- >
127
- <YourContent />
128
- </BottomSheet>;
129
- ```
112
+ ## The content component pattern
130
113
 
131
- ## Core Components
114
+ ### The idea
132
115
 
133
- ### `BottomSheetModal`
116
+ A closed sheet has its entire subtree unmounted. Anything you put in a **child
117
+ component** therefore does no work until the sheet opens: no state initialises, no
118
+ effects run, no queries fire, no lists build. Anything you put in the **wrapper's
119
+ function body** runs on every render of the screen that owns it, open or closed.
134
120
 
135
- Full-screen modal with backdrop. Ideal for most use cases.
121
+ So: the wrapper is a shell. All state lives one level down.
136
122
 
137
- ### `BottomSheet`
123
+ ```tsx
124
+ export default function ChooseFlashcards(props: ChooseFlashcardsProps) {
125
+ const { colors, mode } = useTheme();
138
126
 
139
- Inline bottom sheet without backdrop. Useful for persistent UI elements.
127
+ return (
128
+ <SmoothBottomModal
129
+ showModal={props.toggled}
130
+ setShowModal={props.setToggled}
131
+ backgroundColor={colors.background[mode]}
132
+ showContentDelay={{ timeInMilliSecs: 250, type: 'mount' }}
133
+ snapPoints={[90]}
134
+ >
135
+ <ChooseFlashcardsContent {...props} />
136
+ </SmoothBottomModal>
137
+ );
138
+ }
140
139
 
141
- ### `BottomSheetFlatlist`
140
+ function ChooseFlashcardsContent(props: ChooseFlashcardsProps) {
141
+ const { relativeY, orientation } = useDeviceOrientation();
142
+ const t = useTranslation();
143
+ // ...everything else
144
+ }
145
+ ```
142
146
 
143
- Optimized FlatList with proper gesture handling inside bottom sheets.
147
+ Compare with the version that looks equivalent and is not:
144
148
 
145
- ### `BottomSheetScrollView`
149
+ ```tsx
150
+ export default function ChooseFlashcards(props: ChooseFlashcardsProps) {
151
+ // These run whenever the parent screen renders, with the sheet closed.
152
+ const { data, isLoading } = useFlashcardSets(props.studySetId);
153
+ const [selected, setSelected] = useState<FlashcardSet[]>([]);
154
+ const rows = useMemo(() => buildRows(data), [data]);
146
155
 
147
- Optimized ScrollView with proper gesture handling inside bottom sheets.
156
+ return (
157
+ <SmoothBottomModal
158
+ showModal={props.toggled}
159
+ setShowModal={props.setToggled}
160
+ >
161
+ {rows.map((r) => (
162
+ <Row key={r.id} {...r} />
163
+ ))}
164
+ </SmoothBottomModal>
165
+ );
166
+ }
167
+ ```
148
168
 
149
- ## Portal System
169
+ Every screen that renders a closed sheet pays for that hook stack. Across a dozen
170
+ sheets on a busy screen it is the difference between an instant navigation and a
171
+ visible stall.
150
172
 
151
- The portal system allows bottom sheets to render at the root level of your app, ensuring they appear above all content including navigation stacks.
173
+ ### Why it works
152
174
 
153
- ### `BottomSheetPortalProvider`
175
+ `BottomSheetModal` passes its subtree through a `ComponentMounter` keyed on
176
+ `showModal`. While `showModal` is false, that subtree is not mounted. Constructing
177
+ the element `<ChooseFlashcardsContent {...props} />` is cheap — it is a plain
178
+ object. _Calling_ the component is the expensive part, and React only does that on
179
+ mount.
154
180
 
155
- **Required**: Wrap your app root with this provider to enable portal functionality.
181
+ Hooks written directly in the wrapper are part of the parent's render, outside
182
+ the mounter, so they are unconditional.
156
183
 
157
- ```tsx
158
- import { BottomSheetPortalProvider } from '@shaquillehinds/react-native-bottom-sheet';
184
+ ### Pairing with `showContentDelay`
159
185
 
160
- export default function App() {
161
- return (
162
- <BottomSheetPortalProvider>
163
- <Navigation />
164
- </BottomSheetPortalProvider>
165
- );
166
- }
186
+ The pattern removes work while closed. `showContentDelay` moves the remaining work
187
+ out of the opening animation:
188
+
189
+ ```tsx
190
+ showContentDelay={{ type: 'mount', timeInMilliSecs: 250 }}
167
191
  ```
168
192
 
169
- **Props:**
193
+ The sheet animates open first, then mounts content behind a short fade. The user
194
+ sees a 60fps open instead of a stutter.
195
+
196
+ `type: 'mount'` means there is no content to measure when the sheet opens, so it
197
+ must be told its height. Either set `snapPoints` (preferred) or give
198
+ `contentContainerStyle` a `minHeight`. Without one, the sheet opens collapsed.
170
199
 
171
- | Prop | Type | Default | Description |
172
- | --------------------- | -------- | ------- | ------------------------------------------------------------------ |
173
- | `unMountBufferTimeMS` | `number` | `100` | Delay before removing portal items (prevents premature unmounting) |
174
- | `updateBufferTimeMS` | `number` | - | Throttle time for portal updates (prevents infinite update loops) |
200
+ `type: 'opacity'` (the default when `type` is omitted) renders content immediately
201
+ and fades it in. Use it for light content where you only want the visual polish.
175
202
 
176
- ### `useBottomSheetPortal`
203
+ ### State that must survive closing
177
204
 
178
- Access portal context to manually mount/update/unmount portal items.
205
+ State inside `*Content` is destroyed when the sheet closes — usually what you want.
206
+ When it needs to persist, own it in the screen and pass it down:
179
207
 
180
208
  ```tsx
181
- import { useBottomSheetPortal } from '@shaquillehinds/react-native-bottom-sheet';
209
+ // screen
210
+ const [selectedFlashcards, setSelectedFlashcards] = useState<FlashcardSet[]>(
211
+ []
212
+ );
182
213
 
183
- function MyComponent() {
184
- const portal = useBottomSheetPortal();
214
+ <ChooseFlashcards
215
+ toggled={toggled}
216
+ setToggled={setToggled}
217
+ selectedFlashcards={selectedFlashcards}
218
+ setSelectedFlashcards={setSelectedFlashcards}
219
+ onFlashcardSelect={handleSelect}
220
+ studySetId={studySetId}
221
+ />;
222
+ ```
185
223
 
186
- useEffect(() => {
187
- if (portal) {
188
- const key = portal.mount('my-portal-key', <MyPortalContent />);
189
- return () => portal.unmount(key);
190
- }
191
- }, []);
224
+ Do not solve this by hoisting the state into the sheet wrapper. That reintroduces
225
+ exactly the cost the pattern removes.
192
226
 
193
- return <View />;
194
- }
195
- ```
227
+ ---
196
228
 
197
- **Methods:**
229
+ ## Components
198
230
 
199
- ```typescript
200
- {
201
- mount: (key: string | number, element: ReactNode, onMount?: (key) => void) => PortalKey;
202
- update: (key: string | number, element: ReactNode) => void;
203
- unmount: (key: string | number, onUnMount?: (key) => void) => void;
204
- }
205
- ```
231
+ ### `BottomSheetModal`
206
232
 
207
- ### `useBottomSheetPortalComponent`
233
+ The default. Renders with an animated backdrop, closes on backdrop press, handles
234
+ the Android back button.
208
235
 
209
- Simplified hook for mounting a component to the portal with automatic lifecycle management.
236
+ ### `BottomSheet`
210
237
 
211
- ```tsx
212
- import { useBottomSheetPortalComponent } from '@shaquillehinds/react-native-bottom-sheet';
238
+ Inline sheet with no backdrop. Renders with background content press enabled, so
239
+ taps pass through to what is behind it. Use for filter panels, mini players,
240
+ persistent drawers.
213
241
 
214
- function MyComponent() {
215
- const [showOverlay, setShowOverlay] = useState(true);
242
+ ### `BottomSheetFlatlist` / `BottomSheetScrollView`
216
243
 
217
- useBottomSheetPortalComponent({
218
- name: 'my-overlay',
219
- Component: showOverlay ? <OverlayContent /> : null,
220
- disable: !showOverlay,
221
- });
244
+ Scroll containers that coordinate with the sheet's drag gesture. See
245
+ [Scrollable content](#scrollable-content).
222
246
 
223
- return <View />;
224
- }
225
- ```
247
+ ---
226
248
 
227
- **Props:**
249
+ ## Controlling the sheet
228
250
 
229
- | Prop | Type | Description |
230
- | --------------------- | --------------- | ------------------------------------------------ |
231
- | `name` | `string` | Unique identifier for the portal component |
232
- | `Component` | `ReactNode` | The component to render in the portal |
233
- | `disable` | `boolean` | Disables portal rendering when true |
234
- | `CustomPortalContext` | `React.Context` | Use a custom portal context (for scoped portals) |
251
+ Two modes. Pick one.
235
252
 
236
- ### Types
253
+ ### State-controlled
237
254
 
238
- ```typescript
239
- import type {
240
- PortalItem,
241
- PortalKey,
242
- PortalContextValue,
243
- } from '@shaquillehinds/react-native-bottom-sheet';
255
+ ```tsx
256
+ const [showModal, setShowModal] = useState(false);
244
257
 
245
- type PortalItem = {
246
- key: PortalKey;
247
- element: ReactNode;
248
- };
258
+ <BottomSheetModal showModal={showModal} setShowModal={setShowModal}>
259
+ <Content />
260
+ </BottomSheetModal>;
261
+ ```
249
262
 
250
- type PortalKey = number | string;
263
+ You can wrap the setter to run side effects on open and close:
264
+
265
+ ```tsx
266
+ setShowModal={(value: boolean) => {
267
+ Keyboard.dismiss();
268
+ props.setShowModal(value);
269
+ }}
251
270
  ```
252
271
 
253
- ## API Reference
254
-
255
- ### Props
256
-
257
- #### `BottomSheetProps` (Common to both components)
258
-
259
- | Prop | Type | Default | Description |
260
- | ------------------------------- | ----------------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------- |
261
- | `showModal` | `boolean` | - | Controls visibility of the bottom sheet |
262
- | `setShowModal` | `(bool: boolean) => void` | - | Callback to update visibility state |
263
- | `snapPoints` | `(number \| string)[]` | `[25, 50, 75]` | Array of snap positions as percentages of screen height. Accepts numbers or strings (e.g., `[25, "50", "75%"]`) |
264
- | `dragArea` | `'full' \| 'bumper' \| 'none'` | `'bumper'` | Defines draggable area of the modal |
265
- | `keepMounted` | `boolean` | `false` | Prevents full unmounting when closed. Use with `bottomOffset` to keep visible |
266
- | `hideBumper` | `boolean` | `false` | Hides the draggable bumper at top |
267
- | `avoidKeyboard` | `boolean` | `false` | Adds padding when keyboard is visible |
268
- | `allowDragWhileKeyboardVisible` | `boolean` | `false` | Allows dragging when keyboard is open (disabled by default to prevent conflicts) |
269
- | `inputsForKeyboardToAvoid` | `React.RefObject<TextInput>[]` | - | Specific inputs that trigger keyboard avoidance |
270
- | `bottomOffset` | `number` | `0` | Pushes modal up from bottom. Useful with `keepMounted` |
271
- | `style` | `StyleProp<ViewStyle>` | - | Style for the modal sheet container |
272
- | `backgroundColor` | `string` | - | Background color for modal and bumper |
273
- | `contentContainerStyle` | `StyleProp<ViewStyle>` | - | Style for the content container |
274
- | `bumperStyle` | `StyleProp<ViewStyle>` | - | Style for the bumper element |
275
- | `bumperContainerStyle` | `StyleProp<ViewStyle>` | - | Style for the bumper container |
276
- | `BumperComponent` | `() => React.ReactNode` | - | Custom bumper component |
277
- | `disablePortal` | `boolean` | `false` | Disables portal rendering |
278
- | `CustomPortalContext` | `React.Context<PortalContextValue>` | - | Custom portal context for scoped portals |
279
-
280
- #### `BottomSheetModalProps` (Extends BottomSheetProps)
281
-
282
- | Prop | Type | Default | Description |
283
- | ----------------------------- | ----------------- | ------- | ----------------------------------------- |
284
- | `BackdropComponent` | `React.ReactNode` | - | Custom backdrop component |
285
- | `onBackDropPress` | `() => void` | - | Callback when backdrop is pressed |
286
- | `disableCloseOnBackdropPress` | `boolean` | `false` | Prevents closing on backdrop press |
287
- | `useNativeModal` | `boolean` | `false` | Use React Native's native Modal component |
288
-
289
- #### Callbacks
290
-
291
- | Prop | Type | Description |
292
- | ------------------ | --------------------------------------------------- | -------------------------------------- |
293
- | `onModalShow` | `() => void \| Promise<void>` | Called when modal finishes mounting |
294
- | `onModalClose` | `() => void \| Promise<void>` | Called when modal finishes unmounting |
295
- | `onSnapPointReach` | `(snapPointIndex: number) => void \| Promise<void>` | Called when modal reaches a snap point |
296
-
297
- #### Performance Optimization
298
-
299
- | Prop | Type | Description |
300
- | ------------------ | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
301
- | `showContentDelay` | `{ type?: 'mount' \| 'opacity', timeInMilliSecs: number }` | Delays content rendering for heavy components. Use `'mount'` for better performance with sluggish animations. Provide appropriate `minHeight` when using `'mount'` |
302
-
303
- ### Ref Methods
304
-
305
- #### `BottomModalRef` (for BottomSheetModal and BottomSheet)
306
-
307
- ```typescript
308
- const bottomSheetRef = useRef<BottomModalRefObject>(null);
309
-
310
- // Open modal programmatically
311
- bottomSheetRef.current?.openModal({
312
- onOpen: () => console.log('Modal opened'),
313
- });
272
+ ### Ref-controlled
314
273
 
315
- // Close modal with options
316
- bottomSheetRef.current?.closeModal({
317
- skipAnimation: false, // Skip close animation
318
- isNavigating: false, // Fast close for navigation
319
- duration: 300, // Custom duration
320
- easing: Easing.linear, // Custom easing
321
- onClose: () => console.log('Modal closed'),
322
- });
274
+ Omit `showModal` and `setShowModal` entirely. Useful for a sheet mounted once,
275
+ high in the tree, opened from anywhere in the app:
323
276
 
324
- // Close instantly without animation
325
- bottomSheetRef.current?.closeWithoutAnimation();
277
+ ```tsx
278
+ export default function UploadMaterialModal() {
279
+ const ref = useRef<BottomModalRef>(null);
280
+ const setUploadModalRef = useApp((state) => state.setUploadModalRef);
326
281
 
327
- // Snap to specific index
328
- bottomSheetRef.current?.snapToIndex(1);
282
+ useEffect(() => {
283
+ setUploadModalRef(ref);
284
+ }, [ref.current]);
329
285
 
330
- // Snap to percentage
331
- bottomSheetRef.current?.snapToPercentage(75);
332
- bottomSheetRef.current?.snapToPercentage('80%');
286
+ return (
287
+ <SmoothBottomModal
288
+ ref={ref}
289
+ disablePortal
290
+ dragArea="full"
291
+ snapPoints={[90]}
292
+ >
293
+ <UploadMaterialModalContent />
294
+ </SmoothBottomModal>
295
+ );
296
+ }
333
297
 
334
- // Get current state
335
- const state = bottomSheetRef.current?.getModalState();
336
- // Returns: ModalState.CLOSED | OPENING | OPEN | CLOSING
298
+ // anywhere
299
+ useApp.getState().uploadModalRef?.current?.openModal();
337
300
  ```
338
301
 
339
- #### `BottomSheetRef` (for inline BottomSheet with BottomSheetControl)
302
+ `disablePortal` is right here because the sheet is already mounted at root level —
303
+ there is nothing above it for the portal to lift it past.
304
+
305
+ ### Ref methods
340
306
 
341
- ```typescript
342
- const sheetRef = useRef<BottomSheetRefObject>(null);
307
+ ```tsx
308
+ const ref = useRef<BottomModalRef>(null);
343
309
 
344
- // Animate close
345
- sheetRef.current?.animateCloseModal({
310
+ ref.current?.openModal({ onOpen: () => {} });
311
+ ref.current?.closeModal({
312
+ skipAnimation: false,
313
+ isNavigating: false,
346
314
  duration: 300,
347
- easing: Easing.bezier(0.2, 0.32, 0, 1),
315
+ easing: Easing.linear,
316
+ onClose: () => {},
348
317
  });
318
+ ref.current?.closeWithoutAnimation();
319
+ ref.current?.snapToIndex(1);
320
+ ref.current?.snapToPercentage(75); // or '75%'
321
+ ref.current?.getModalState(); // ModalState.CLOSED | OPENING | OPEN | CLOSING
322
+ ```
323
+
324
+ Behaviour to be aware of on a **state-controlled** sheet:
349
325
 
350
- // Snap to index
351
- sheetRef.current?.snapToIndex(2);
326
+ | Call | What actually happens |
327
+ | ------------------------------------- | ----------------------------------------------- |
328
+ | `openModal()` | `setShowModal(true)` |
329
+ | `openModal({ onOpen })` | routes through the mounter so `onOpen` can fire |
330
+ | `closeModal()` | `setShowModal(false)` |
331
+ | `closeModal({ duration \| onClose })` | animates, then clears state after the animation |
332
+ | `closeModal({ isNavigating: true })` | 100ms close — use before navigating |
333
+ | `closeModal({ skipAnimation: true })` | immediate unmount |
352
334
 
353
- // Snap to percentage
354
- sheetRef.current?.snapToPercentage(50);
335
+ ### Ref typing
355
336
 
356
- // Get state
357
- const state = sheetRef.current?.getModalState();
337
+ ```tsx
338
+ const ref = useRef<BottomModalRef>(null); // ✅
358
339
  ```
359
340
 
360
- ### Hook: `useBottomSheetRef`
341
+ `BottomModalRefObject` is `React.Ref<BottomModalRef>` — it types the `ref` _prop_
342
+ on the component, not the object `useRef` gives you. Using it in `useRef` produces
343
+ a doubly-wrapped type.
361
344
 
362
- Access the modal ref from within the bottom sheet component tree.
345
+ ### Closing from inside the sheet
363
346
 
364
347
  ```tsx
365
348
  import { useBottomSheetRef } from '@shaquillehinds/react-native-bottom-sheet';
366
349
 
367
- function ContentComponent() {
350
+ function Footer() {
368
351
  const { modalRef } = useBottomSheetRef();
352
+ return (
353
+ <Button title="Close" onPress={() => modalRef?.current?.closeModal()} />
354
+ );
355
+ }
356
+ ```
369
357
 
370
- const handleClose = () => {
371
- modalRef?.current?.closeModal();
372
- };
358
+ Available anywhere inside the sheet's subtree. Returns `{ modalRef }` where
359
+ `modalRef` may be undefined outside a sheet, so optional-chain it.
373
360
 
374
- return <Button title="Close" onPress={handleClose} />;
375
- }
361
+ ---
362
+
363
+ ## Snap points
364
+
365
+ Percentages of screen height. Numbers or strings both work:
366
+
367
+ ```tsx
368
+ snapPoints={[25, 50, 75]}
369
+ snapPoints={['25', '50%', 90]}
376
370
  ```
377
371
 
378
- ## Advanced Usage
372
+ - The sheet opens at index `0`. Order the array in the direction you want it read;
373
+ `snapToIndex` uses the array's own indices.
374
+ - `onSnapPointReach(index)` fires when the sheet settles on one. Only active when
375
+ `snapPoints` is set.
376
+ - Keep the array referentially stable — a module constant or `useMemo`.
377
+ - On orientation change, snap points are recomputed and the sheet re-snaps to
378
+ index 0.
379
+
380
+ ### Content-height mode
381
+
382
+ Omitting `snapPoints` is a supported mode, not an oversight. The sheet measures its
383
+ content on layout and opens to exactly that height, capped at roughly 110% of
384
+ screen height. Good for short, content-sized sheets — confirmation dialogs, small
385
+ action lists.
386
+
387
+ Dragging behaviour differs: with no snap points, a drag past half the content
388
+ height or a fast flick dismisses the sheet.
389
+
390
+ Content-height mode is incompatible with `showContentDelay: { type: 'mount' }`,
391
+ since there is nothing to measure at open time. Use `snapPoints`, or set a
392
+ `minHeight` on `contentContainerStyle`.
379
393
 
380
- ### Scrollable Content
394
+ ---
381
395
 
382
- #### Using BottomSheetFlatlist
396
+ ## Scrollable content
383
397
 
384
398
  ```tsx
385
399
  import {
@@ -387,1076 +401,522 @@ import {
387
401
  BottomSheetFlatlist,
388
402
  } from '@shaquillehinds/react-native-bottom-sheet';
389
403
 
390
- <BottomSheetModal
391
- showModal={showModal}
392
- setShowModal={setShowModal}
393
- snapPoints={[50, 90]}
394
- >
404
+ <BottomSheetModal snapPoints={[50, 90]} showModal={show} setShowModal={setShow}>
395
405
  <BottomSheetFlatlist
396
406
  data={items}
397
- renderItem={({ item }) => <ItemComponent item={item} />}
407
+ renderItem={({ item }) => <Row item={item} />}
398
408
  keyExtractor={(item) => item.id}
399
409
  />
400
410
  </BottomSheetModal>;
401
411
  ```
402
412
 
403
- #### Using BottomSheetScrollView
413
+ These wrap the underlying Reanimated list in a drag gesture that coordinates with
414
+ the sheet: the sheet drags when the list sits at a scroll boundary, the list
415
+ scrolls otherwise. A bare `FlatList` or `ScrollView` has no such coordination and
416
+ the two gestures will fight.
404
417
 
405
- ```tsx
406
- import {
407
- BottomSheetModal,
408
- BottomSheetScrollView,
409
- } from '@shaquillehinds/react-native-bottom-sheet';
418
+ Notes:
410
419
 
411
- <BottomSheetModal showModal={showModal} setShowModal={setShowModal}>
412
- <BottomSheetScrollView>{/* Your scrollable content */}</BottomSheetScrollView>
413
- </BottomSheetModal>;
414
- ```
420
+ - Props are the full underlying set — `FlatListPropsWithLayout` for the list,
421
+ `AnimatedScrollViewProps` for the scroll view — with `onScroll` widened to also
422
+ accept a Reanimated scroll event.
423
+ - Pass a custom ref via `refFlatlist` / `refScrollView`, not `ref`.
424
+ - `inverted` lists are supported; the sheet is told about the inversion.
425
+ - `bounces` is forced to `false` inside a sheet.
426
+ - Used outside a sheet, both fall back to a plain animated list/scroll view.
427
+ - Avoid `dragArea="full"` alongside a scrollable child. Keep the default `"bumper"`.
428
+
429
+ ---
415
430
 
416
- ### Keyboard Handling
431
+ ## Keyboard handling
417
432
 
418
- #### Global Keyboard Avoidance
433
+ ### Whole-sheet avoidance
419
434
 
420
435
  ```tsx
421
- <BottomSheetModal
422
- avoidKeyboard={true}
423
- showModal={showModal}
424
- setShowModal={setShowModal}
425
- >
436
+ <BottomSheetModal avoidKeyboard showModal={show} setShowModal={setShow}>
426
437
  <TextInput placeholder="Email" />
427
- <TextInput placeholder="Password" />
428
438
  </BottomSheetModal>
429
439
  ```
430
440
 
431
- #### Per-Input Keyboard Avoidance
441
+ ### Per-input avoidance
432
442
 
433
- ```tsx
434
- function FormComponent() {
435
- const emailRef = useRef<TextInput>(null);
436
- const passwordRef = useRef<TextInput>(null);
437
-
438
- return (
439
- <BottomSheetModal
440
- inputsForKeyboardToAvoid={[emailRef, passwordRef]}
441
- showModal={showModal}
442
- setShowModal={setShowModal}
443
- >
444
- <TextInput ref={emailRef} placeholder="Email" />
445
- <TextInput ref={passwordRef} placeholder="Password" />
446
- <TextInput placeholder="Not tracked" />
447
- </BottomSheetModal>
448
- );
449
- }
450
- ```
451
-
452
- ### Custom Bumper
443
+ Only shift for specific inputs — useful when a search field should move but a
444
+ comment box further down should not:
453
445
 
454
446
  ```tsx
455
- function CustomBumper() {
456
- return (
457
- <View style={{ padding: 20, alignItems: 'center' }}>
458
- <View
459
- style={{
460
- width: 100,
461
- height: 5,
462
- backgroundColor: 'blue',
463
- borderRadius: 3,
464
- }}
465
- />
466
- <Text style={{ marginTop: 10 }}>Drag me</Text>
467
- </View>
468
- );
469
- }
447
+ const emailRef = useRef<TextInput>(null);
448
+ const passwordRef = useRef<TextInput>(null);
470
449
 
471
450
  <BottomSheetModal
472
- BumperComponent={CustomBumper}
473
- showModal={showModal}
474
- setShowModal={setShowModal}
451
+ inputsForKeyboardToAvoid={[emailRef, passwordRef]}
452
+ showModal={show}
453
+ setShowModal={setShow}
475
454
  >
476
- <YourContent />
455
+ <TextInput ref={emailRef} />
456
+ <TextInput ref={passwordRef} />
457
+ <TextInput placeholder="Not tracked" />
477
458
  </BottomSheetModal>;
478
459
  ```
479
460
 
480
- ### Custom Backdrop
461
+ Dragging is disabled while the keyboard is open, since the two interactions
462
+ conflict. Override with `allowDragWhileKeyboardVisible` if you have a reason.
481
463
 
482
- ```tsx
483
- function CustomBackdrop() {
484
- return (
485
- <View
486
- style={{
487
- flex: 1,
488
- backgroundColor: 'rgba(255, 0, 0, 0.3)',
489
- }}
490
- />
491
- );
492
- }
464
+ Do not add `KeyboardAvoidingView` — it will fight the built-in handling.
493
465
 
494
- <BottomSheetModal
495
- BackdropComponent={<CustomBackdrop />}
496
- showModal={showModal}
497
- setShowModal={setShowModal}
498
- >
499
- <YourContent />
500
- </BottomSheetModal>;
501
- ```
466
+ ---
502
467
 
503
- ### Persistent Bottom Sheet
468
+ ## Persistent sheets
504
469
 
505
- Keep the sheet partially visible when "closed":
470
+ `keepMounted` changes what a downward drag does: instead of dismissing, the sheet
471
+ snaps to its lowest snap point.
506
472
 
507
473
  ```tsx
508
474
  <BottomSheet
509
- keepMounted={true}
510
- bottomOffset={50} // Shows 50px when closed
511
- snapPoints={[10, 50, 90]}
512
- showModal={showModal}
513
- setShowModal={setShowModal}
475
+ keepMounted
476
+ snapPoints={[10, 70]}
477
+ bottomOffset={100}
478
+ showModal={show}
479
+ setShowModal={setShow}
514
480
  >
515
- <YourContent />
481
+ <FilterPanel />
516
482
  </BottomSheet>
517
483
  ```
518
484
 
519
- ### Performance Optimization for Heavy Content
485
+ Two things to know:
520
486
 
521
- Use `showContentDelay` to improve modal animation performance:
487
+ - **`keepMounted` requires `snapPoints`.** With no snap points it is ignored and
488
+ a drag down will dismiss as usual.
489
+ - It governs the drag gesture only. `showModal={false}` still unmounts the sheet.
522
490
 
523
- ```tsx
524
- <BottomSheetModal
525
- showModal={showModal}
526
- setShowModal={setShowModal}
527
- contentContainerStyle={{ minHeight: 400 }}
528
- showContentDelay={{
529
- type: 'mount', // Delays mounting entirely
530
- timeInMilliSecs: 100,
531
- }}
532
- >
533
- <HeavyComponent />
534
- </BottomSheetModal>
535
- ```
491
+ `bottomOffset` pushes the sheet up from the bottom of the screen, so its "closed"
492
+ position stays visible as a peek.
536
493
 
537
- Or use opacity animation:
494
+ ---
538
495
 
539
- ```tsx
540
- <BottomSheetModal
541
- showContentDelay={{
542
- type: 'opacity', // Fades in content
543
- timeInMilliSecs: 200,
544
- }}
545
- showModal={showModal}
546
- setShowModal={setShowModal}
547
- >
548
- <YourContent />
549
- </BottomSheetModal>
550
- ```
496
+ ## Appearance
551
497
 
552
- ### Snap Point Callbacks
498
+ | Prop | Targets |
499
+ | ----------------------- | -------------------------------------- |
500
+ | `backgroundColor` | Sheet surface **and** bumper container |
501
+ | `style` | The sheet surface |
502
+ | `contentContainerStyle` | The container wrapping your children |
503
+ | `bumperStyle` | The grab handle itself |
504
+ | `bumperContainerStyle` | The area around the handle |
553
505
 
554
- ```tsx
555
- <BottomSheetModal
556
- snapPoints={[25, 50, 75]}
557
- onSnapPointReach={(index) => {
558
- console.log(`Reached snap point: ${index}`);
559
- if (index === 2) {
560
- // Reached highest snap point
561
- }
562
- }}
563
- showModal={showModal}
564
- setShowModal={setShowModal}
565
- >
566
- <YourContent />
567
- </BottomSheetModal>
568
- ```
506
+ Use `backgroundColor` rather than setting a background in `style` — the former
507
+ also colours the bumper container, so the handle area matches.
569
508
 
570
- ### Drag Configuration
509
+ ### Custom bumper
571
510
 
572
- #### Full Sheet Dragging
511
+ The custom component becomes the drag area when `dragArea` is `"bumper"`.
573
512
 
574
513
  ```tsx
575
- <BottomSheetModal
576
- dragArea="full"
577
- showModal={showModal}
578
- setShowModal={setShowModal}
579
- >
580
- <YourContent />
581
- </BottomSheetModal>
582
- ```
583
-
584
- #### Disable Dragging
514
+ function Bumper() {
515
+ return (
516
+ <View style={{ padding: 20, alignItems: 'center' }}>
517
+ <View style={{ width: 100, height: 5, backgroundColor: '#888', borderRadius: 3 }} />
518
+ </View>
519
+ );
520
+ }
585
521
 
586
- ```tsx
587
- <BottomSheetModal
588
- dragArea="none"
589
- showModal={showModal}
590
- setShowModal={setShowModal}
591
- >
592
- <YourContent />
593
- </BottomSheetModal>
522
+ <BottomSheetModal BumperComponent={Bumper} ... />
594
523
  ```
595
524
 
596
- #### Allow Dragging with Keyboard
525
+ ### Custom backdrop
597
526
 
598
527
  ```tsx
599
528
  <BottomSheetModal
600
- avoidKeyboard={true}
601
- allowDragWhileKeyboardVisible={true}
602
- showModal={showModal}
603
- setShowModal={setShowModal}
604
- >
605
- <TextInput />
606
- </BottomSheetModal>
529
+ BackdropComponent={<BlurView intensity={40} style={StyleSheet.absoluteFill} />}
530
+ onBackDropPress={() => track('dismiss')}
531
+ ...
532
+ />
607
533
  ```
608
534
 
609
- ### Portal Management
535
+ `disableCloseOnBackdropPress` keeps `onBackDropPress` firing without closing.
610
536
 
611
- By default, bottom sheets use the portal system to render at the root level, ensuring they appear above all content.
537
+ ### Drag area
612
538
 
613
- #### Using Default Portal (Recommended)
539
+ | Value | Behaviour |
540
+ | -------------------- | ----------------------------------------------------- |
541
+ | `"bumper"` (default) | Only the handle drags |
542
+ | `"full"` | The whole sheet drags — avoid with scrollable content |
543
+ | `"none"` | No dragging; close programmatically or via backdrop |
614
544
 
615
- The bottom sheet automatically uses the portal when `BottomSheetPortalProvider` is set up:
545
+ `hideBumper` removes the handle. With `dragArea="bumper"` and `hideBumper`, there
546
+ is nothing left to drag — pair `hideBumper` with `"full"` or `"none"`.
616
547
 
617
- ```tsx
618
- // App.tsx
619
- import { BottomSheetPortalProvider } from '@shaquillehinds/react-native-bottom-sheet';
548
+ ---
620
549
 
621
- export default function App() {
622
- return (
623
- <BottomSheetPortalProvider>
624
- <NavigationContainer>
625
- <Stack.Navigator>
626
- <Stack.Screen name="Home" component={HomeScreen} />
627
- </Stack.Navigator>
628
- </NavigationContainer>
629
- </BottomSheetPortalProvider>
630
- );
631
- }
550
+ ## Portal system
632
551
 
633
- // HomeScreen.tsx - bottom sheet will render above navigation
634
- <BottomSheetModal showModal={showModal} setShowModal={setShowModal}>
635
- <YourContent />
636
- </BottomSheetModal>;
637
- ```
552
+ Sheets render into a portal at app root so they sit above navigation. This is
553
+ automatic once `BottomSheetPortalProvider` is in place.
638
554
 
639
- #### Disable Portal Rendering
555
+ ### `BottomSheetPortalProvider`
640
556
 
641
- If you want the bottom sheet to render in its natural position in the component tree:
557
+ | Prop | Type | Default | Description |
558
+ | --------------------- | --------------- | ------- | --------------------------------------- |
559
+ | `unMountBufferTimeMS` | `number` | `100` | Delay before removing portal items |
560
+ | `updateBufferTimeMS` | `number` | – | Throttle for portal updates |
561
+ | `CustomPortalContext` | `React.Context` | – | Scope this provider to a custom context |
642
562
 
643
- ```tsx
644
- <BottomSheetModal
645
- disablePortal={true}
646
- showModal={showModal}
647
- setShowModal={setShowModal}
648
- >
649
- <YourContent />
650
- </BottomSheetModal>
651
- ```
563
+ ### `disablePortal`
652
564
 
653
- #### Custom Portal Context (Advanced)
565
+ Renders the sheet where it sits in the tree. Correct when the sheet is already
566
+ mounted at root level, or when you want it scoped to a subtree.
654
567
 
655
- Create scoped portals for specific parts of your app:
656
-
657
- ```tsx
658
- import {
659
- BottomSheetPortalProvider,
660
- useBottomSheetPortal,
661
- } from '@shaquillehinds/react-native-bottom-sheet';
662
- import { createContext } from 'react';
663
- import type { PortalContextValue } from '@shaquillehinds/react-native-bottom-sheet';
568
+ ### Scoped portals
664
569
 
665
- // Create a custom portal context
666
- const MyCustomPortalContext = createContext<PortalContextValue | undefined>(
667
- undefined
668
- );
669
-
670
- // Wrap specific section with custom portal provider
671
- function MySection() {
672
- return (
673
- <BottomSheetPortalProvider CustomPortalContext={MyCustomPortalContext}>
674
- <MySectionContent />
675
- </BottomSheetPortalProvider>
676
- );
677
- }
678
-
679
- // Use the custom context in your bottom sheet
680
- <BottomSheetModal
681
- CustomPortalContext={MyCustomPortalContext}
682
- showModal={showModal}
683
- setShowModal={setShowModal}
684
- >
685
- <YourContent />
686
- </BottomSheetModal>;
687
- ```
688
-
689
- #### Manual Portal Control
690
-
691
- For advanced use cases where you need direct portal control:
570
+ Both provider and sheet must be handed the same context object:
692
571
 
693
572
  ```tsx
694
- import { useBottomSheetPortal } from '@shaquillehinds/react-native-bottom-sheet';
695
-
696
- function CustomPortalComponent() {
697
- const portal = useBottomSheetPortal();
698
- const [portalKey, setPortalKey] = useState<string | number | null>(null);
699
-
700
- const mountContent = () => {
701
- if (portal) {
702
- const key = portal.mount(
703
- 'custom-content',
704
- <View style={{ padding: 20, backgroundColor: 'white' }}>
705
- <Text>Portal Content</Text>
706
- </View>,
707
- (key) => console.log('Mounted:', key)
708
- );
709
- setPortalKey(key);
710
- }
711
- };
712
-
713
- const updateContent = () => {
714
- if (portal && portalKey) {
715
- portal.update(
716
- portalKey,
717
- <View style={{ padding: 20, backgroundColor: 'blue' }}>
718
- <Text>Updated Content</Text>
719
- </View>
720
- );
721
- }
722
- };
723
-
724
- const unmountContent = () => {
725
- if (portal && portalKey) {
726
- portal.unmount(portalKey, (key) => console.log('Unmounted:', key));
727
- setPortalKey(null);
728
- }
729
- };
573
+ const ScreenPortalContext = createContext<PortalContextValue | undefined>(undefined);
730
574
 
731
- return (
732
- <View>
733
- <Button title="Mount" onPress={mountContent} />
734
- <Button title="Update" onPress={updateContent} />
735
- <Button title="Unmount" onPress={unmountContent} />
736
- </View>
737
- );
738
- }
575
+ <BottomSheetPortalProvider CustomPortalContext={ScreenPortalContext}>
576
+ <BottomSheetModal CustomPortalContext={ScreenPortalContext} ...>
577
+ <Content />
578
+ </BottomSheetModal>
579
+ </BottomSheetPortalProvider>
739
580
  ```
740
581
 
741
- #### Using Portal Component Hook
582
+ ### Non-sheet overlays
742
583
 
743
- Simplified component-based portal management with automatic cleanup:
584
+ `useBottomSheetPortalComponent` mounts arbitrary content into the same portal with
585
+ automatic cleanup — toasts, loading overlays, anything that needs to sit above
586
+ everything:
744
587
 
745
588
  ```tsx
746
- import { useBottomSheetPortalComponent } from '@shaquillehinds/react-native-bottom-sheet';
747
-
748
- function ToastNotification() {
749
- const [message, setMessage] = useState('');
750
- const [show, setShow] = useState(false);
751
-
752
- // Automatically mounts/unmounts based on show state
753
- useBottomSheetPortalComponent({
754
- name: 'toast-notification',
755
- Component: show ? (
756
- <View style={{ position: 'absolute', top: 50, alignSelf: 'center' }}>
757
- <Text>{message}</Text>
758
- </View>
759
- ) : null,
760
- disable: !show,
761
- });
762
-
763
- const showToast = (msg: string) => {
764
- setMessage(msg);
765
- setShow(true);
766
- setTimeout(() => setShow(false), 3000);
767
- };
768
-
769
- return <Button title="Show Toast" onPress={() => showToast('Hello!')} />;
770
- }
589
+ useBottomSheetPortalComponent({
590
+ name: 'toast',
591
+ Component: visible ? <Toast message={message} /> : null,
592
+ disable: !visible,
593
+ });
771
594
  ```
772
595
 
773
- ### Navigation Integration
596
+ `useBottomSheetPortal` gives the raw `{ mount, update, unmount }` API for manual
597
+ control.
598
+
599
+ You do not need either of these for a bottom sheet — the sheet portals itself.
600
+
601
+ ---
602
+
603
+ ## API reference
604
+
605
+ ### `BottomSheetProps`
606
+
607
+ | Prop | Type | Default | Description |
608
+ | ------------------------------- | ---------------------------------------------------------- | ---------- | --------------------------------------------------------------------------------- |
609
+ | `showModal` | `boolean` | – | Visibility. Omit for a ref-controlled sheet |
610
+ | `setShowModal` | `(bool: boolean) => void` | – | Visibility setter |
611
+ | `snapPoints` | `(number \| string)[]` | – | Percentages of screen height. Omit for content-height mode |
612
+ | `dragArea` | `'full' \| 'bumper' \| 'none'` | `'bumper'` | Draggable region |
613
+ | `keepMounted` | `boolean` | `false` | Drag down snaps to lowest snap point instead of dismissing. Requires `snapPoints` |
614
+ | `hideBumper` | `boolean` | `false` | Remove the grab handle |
615
+ | `avoidKeyboard` | `boolean` | `false` | Pad content when the keyboard is open |
616
+ | `allowDragWhileKeyboardVisible` | `boolean` | `false` | Permit dragging with the keyboard open |
617
+ | `inputsForKeyboardToAvoid` | `React.RefObject<TextInput>[]` | – | Only avoid for these inputs |
618
+ | `bottomOffset` | `number` | `0` | Push the sheet up from the bottom |
619
+ | `showContentDelay` | `{ type?: 'mount' \| 'opacity'; timeInMilliSecs: number }` | – | Defer content render past the open animation |
620
+ | `style` | `StyleProp<ViewStyle>` | – | Sheet surface |
621
+ | `contentContainerStyle` | `StyleProp<ViewStyle>` | – | Content container |
622
+ | `bumperStyle` | `StyleProp<ViewStyle>` | – | Grab handle |
623
+ | `bumperContainerStyle` | `StyleProp<ViewStyle>` | – | Handle container |
624
+ | `backgroundColor` | `string` | – | Sheet and bumper container background |
625
+ | `BumperComponent` | `() => React.ReactNode` | – | Replace the default handle |
626
+ | `onModalShow` | `() => void \| Promise<void>` | – | Fires when the sheet finishes mounting |
627
+ | `onModalClose` | `() => void \| Promise<void>` | – | Fires when the sheet finishes unmounting |
628
+ | `onSnapPointReach` | `(index: number) => void \| Promise<void>` | – | Fires on settling at a snap point |
629
+ | `disablePortal` | `boolean` | `false` | Render in place |
630
+ | `CustomPortalContext` | `React.Context<PortalContextValue>` | – | Scoped portal |
631
+
632
+ ### `BottomSheetModalProps`
633
+
634
+ Extends `BottomSheetProps`.
635
+
636
+ | Prop | Type | Default | Description |
637
+ | ------------------------------ | ----------------- | ------- | -------------------------------------------- |
638
+ | `BackdropComponent` | `React.ReactNode` | – | Custom backdrop |
639
+ | `onBackDropPress` | `() => void` | – | Backdrop press callback |
640
+ | `disableCloseOnBackdropPress` | `boolean` | `false` | Keep open on backdrop press |
641
+ | `useNativeModal` | `boolean` | `false` | Render inside RN's native `Modal` |
642
+ | `enableBackgroundContentPress` | `boolean` | `false` | Drop the backdrop; taps reach content behind |
643
+ | `disableAndroidBackButton` | `boolean` | `false` | Ignore the hardware back button |
644
+
645
+ ### `BottomSheetFlatlistProps<T>`
646
+
647
+ `Omit<FlatListPropsWithLayout<T>, 'onScroll'>` plus:
648
+
649
+ | Prop | Type |
650
+ | ------------- | --------------------------------------- |
651
+ | `onScroll` | `ReanimatedOnScroll \| DefaultOnScroll` |
652
+ | `refFlatlist` | `React.RefObject<FlatList>` |
653
+
654
+ ### `BottomSheetScrollViewProps`
655
+
656
+ `Omit<AnimatedScrollViewProps, 'onScroll'>` plus:
657
+
658
+ | Prop | Type |
659
+ | --------------- | --------------------------------------- |
660
+ | `onScroll` | `ReanimatedOnScroll \| DefaultOnScroll` |
661
+ | `refScrollView` | `React.RefObject<AnimatedScrollView>` |
662
+
663
+ ### Ref types
664
+
665
+ ```ts
666
+ type BottomModalRef = {
667
+ getModalState: () => ModalState | undefined;
668
+ openModal: (props?: OpenModalProps) => void;
669
+ closeModal: (props?: CloseModalProps) => void;
670
+ closeWithoutAnimation: () => void;
671
+ snapToIndex: (index: number) => void;
672
+ snapToPercentage: (percentage: number | string) => void;
673
+ };
774
674
 
775
- For smooth navigation transitions on Android:
675
+ type OpenModalProps = { onOpen?: () => void };
776
676
 
777
- ```tsx
778
- const handleNavigateAndClose = () => {
779
- bottomSheetRef.current?.closeModal({
780
- isNavigating: true, // Reduces animation to 100ms
781
- onClose: () => {
782
- navigation.navigate('NextScreen');
783
- },
784
- });
677
+ type CloseModalProps = {
678
+ skipAnimation?: boolean;
679
+ isNavigating?: boolean;
680
+ onClose?: () => void;
681
+ duration?: number;
682
+ easing?: EasingFunction | EasingFunctionFactory;
785
683
  };
684
+
685
+ enum ModalState {
686
+ CLOSED = 0,
687
+ OPENING = 1,
688
+ OPEN = 2,
689
+ CLOSING = 3,
690
+ }
786
691
  ```
787
692
 
788
- Or skip animation entirely:
693
+ ### Exports
789
694
 
790
- ```tsx
791
- bottomSheetRef.current?.closeModal({
792
- skipAnimation: true,
793
- onClose: () => navigation.navigate('NextScreen'),
794
- });
795
- ```
695
+ ```ts
696
+ // Components
697
+ (BottomSheet, BottomSheetModal, BottomSheetFlatlist, BottomSheetScrollView);
796
698
 
797
- ## TypeScript Support
699
+ // Hooks
700
+ (useBottomSheetRef, useBottomSheetPortal, useBottomSheetPortalComponent);
798
701
 
799
- Full TypeScript support with comprehensive type definitions:
702
+ // Provider
703
+ BottomSheetPortalProvider;
800
704
 
801
- ```typescript
802
- // Component Props
803
- import type {
804
- BottomSheetProps,
705
+ // Types
706
+ (BottomSheetProps,
805
707
  BottomSheetModalProps,
806
708
  BottomSheetFlatlistProps,
807
709
  BottomSheetScrollViewProps,
808
- } from '@shaquillehinds/react-native-bottom-sheet';
809
-
810
- // Ref Types
811
- import type {
812
- BottomModalRefObject,
813
- BottomSheetRefObject,
814
710
  BottomModalRef,
815
711
  BottomSheetRef,
816
- } from '@shaquillehinds/react-native-bottom-sheet';
817
-
818
- // State and Config Types
819
- import type {
712
+ BottomModalRefObject,
713
+ BottomSheetRefObject,
820
714
  ModalState,
821
- AnimateCloseModalProps,
822
- CloseModalProps,
823
715
  OpenModalProps,
824
- } from '@shaquillehinds/react-native-bottom-sheet';
825
-
826
- // Portal Types
827
- import type {
716
+ CloseModalProps,
717
+ AnimateCloseModalProps,
828
718
  PortalItem,
829
719
  PortalKey,
830
- PortalContextValue,
831
- } from '@shaquillehinds/react-native-bottom-sheet';
832
- ```
833
-
834
- ## Examples
835
-
836
- ### Complete Modal Example
837
-
838
- ```tsx
839
- import React, { useRef, useState } from 'react';
840
- import { View, Text, Button, TextInput } from 'react-native';
841
- import {
842
- BottomSheetModal,
843
- BottomSheetScrollView,
844
- useBottomSheetRef,
845
- } from '@shaquillehinds/react-native-bottom-sheet';
846
- import type { BottomModalRefObject } from '@shaquillehinds/react-native-bottom-sheet';
847
-
848
- export default function CompleteExample() {
849
- const [showModal, setShowModal] = useState(false);
850
- const bottomSheetRef = useRef<BottomModalRefObject>(null);
851
-
852
- const handleOpen = () => {
853
- bottomSheetRef.current?.openModal({
854
- onOpen: () => console.log('Modal opened!'),
855
- });
856
- };
857
-
858
- const handleSnapToTop = () => {
859
- bottomSheetRef.current?.snapToIndex(2);
860
- };
861
-
862
- return (
863
- <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
864
- <Button title="Open Modal" onPress={() => setShowModal(true)} />
865
-
866
- <BottomSheetModal
867
- ref={bottomSheetRef}
868
- showModal={showModal}
869
- setShowModal={setShowModal}
870
- snapPoints={[30, 60, 90]}
871
- backgroundColor="#ffffff"
872
- avoidKeyboard={true}
873
- onModalShow={() => console.log('Modal shown')}
874
- onModalClose={() => console.log('Modal closed')}
875
- onSnapPointReach={(index) => console.log('Snap point:', index)}
876
- >
877
- <BottomSheetScrollView>
878
- <View style={{ padding: 20 }}>
879
- <Text style={{ fontSize: 24, marginBottom: 20 }}>My Modal</Text>
880
-
881
- <TextInput
882
- placeholder="Enter text..."
883
- style={{
884
- borderWidth: 1,
885
- borderColor: '#ccc',
886
- padding: 10,
887
- marginBottom: 20,
888
- }}
889
- />
890
-
891
- <Button title="Snap to Top" onPress={handleSnapToTop} />
892
-
893
- <InnerComponent />
894
- </View>
895
- </BottomSheetScrollView>
896
- </BottomSheetModal>
897
- </View>
898
- );
899
- }
900
-
901
- function InnerComponent() {
902
- const { modalRef } = useBottomSheetRef();
903
-
904
- return (
905
- <Button
906
- title="Close from Inside"
907
- onPress={() => modalRef?.current?.closeModal()}
908
- />
909
- );
910
- }
720
+ PortalContextValue);
911
721
  ```
912
722
 
913
- ### Form with Validation Example
723
+ ---
914
724
 
915
- ```tsx
916
- import React, { useRef, useState } from 'react';
917
- import { View, Text, TextInput, Button } from 'react-native';
918
- import { BottomSheetModal } from '@shaquillehinds/react-native-bottom-sheet';
919
- import type { BottomModalRefObject } from '@shaquillehinds/react-native-bottom-sheet';
725
+ ## Recipes
920
726
 
921
- export default function FormExample() {
922
- const [showModal, setShowModal] = useState(false);
923
- const [email, setEmail] = useState('');
924
- const [password, setPassword] = useState('');
925
- const emailRef = useRef<TextInput>(null);
926
- const passwordRef = useRef<TextInput>(null);
927
- const bottomSheetRef = useRef<BottomModalRefObject>(null);
928
-
929
- const handleSubmit = () => {
930
- if (email && password) {
931
- console.log('Submitting:', { email, password });
932
- bottomSheetRef.current?.closeModal({
933
- onClose: () => {
934
- // Clear form after animation
935
- setEmail('');
936
- setPassword('');
937
- },
938
- });
939
- }
940
- };
727
+ ### Action list
941
728
 
942
- return (
943
- <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
944
- <Button title="Login" onPress={() => setShowModal(true)} />
945
-
946
- <BottomSheetModal
947
- ref={bottomSheetRef}
948
- showModal={showModal}
949
- setShowModal={setShowModal}
950
- snapPoints={[60]}
951
- inputsForKeyboardToAvoid={[emailRef, passwordRef]}
952
- contentContainerStyle={{ padding: 20 }}
953
- >
954
- <Text style={{ fontSize: 24, marginBottom: 20 }}>Login</Text>
955
-
956
- <TextInput
957
- ref={emailRef}
958
- value={email}
959
- onChangeText={setEmail}
960
- placeholder="Email"
961
- keyboardType="email-address"
962
- autoCapitalize="none"
963
- style={{
964
- borderWidth: 1,
965
- borderColor: '#ccc',
966
- padding: 10,
967
- marginBottom: 15,
968
- }}
969
- />
970
-
971
- <TextInput
972
- ref={passwordRef}
973
- value={password}
974
- onChangeText={setPassword}
975
- placeholder="Password"
976
- secureTextEntry
977
- style={{
978
- borderWidth: 1,
979
- borderColor: '#ccc',
980
- padding: 10,
981
- marginBottom: 20,
982
- }}
983
- />
984
-
985
- <Button title="Submit" onPress={handleSubmit} />
986
- </BottomSheetModal>
987
- </View>
988
- );
989
- }
990
- ```
991
-
992
- ### Custom Portal Overlay Example
729
+ The wrapper stays thin; the option array — which closes over `router` and the
730
+ setter is built inside the content component, so it is never constructed while
731
+ the sheet is closed.
993
732
 
994
733
  ```tsx
995
- import React, { useState } from 'react';
996
- import { View, Text, Button, TouchableOpacity, StyleSheet } from 'react-native';
997
- import {
998
- BottomSheetPortalProvider,
999
- useBottomSheetPortalComponent,
1000
- } from '@shaquillehinds/react-native-bottom-sheet';
734
+ export default function ChatAppsModal(props: {
735
+ showModal: boolean;
736
+ setShowModal: React.Dispatch<React.SetStateAction<boolean>>;
737
+ }) {
738
+ const { colors, mode } = useTheme();
1001
739
 
1002
- // App root with provider
1003
- export default function App() {
1004
740
  return (
1005
- <BottomSheetPortalProvider>
1006
- <MyScreen />
1007
- </BottomSheetPortalProvider>
1008
- );
1009
- }
1010
-
1011
- // Screen with custom portal overlay
1012
- function MyScreen() {
1013
- const [showOverlay, setShowOverlay] = useState(false);
1014
- const [message, setMessage] = useState('');
1015
-
1016
- // Mount custom overlay to portal
1017
- useBottomSheetPortalComponent({
1018
- name: 'custom-overlay',
1019
- Component: showOverlay ? (
1020
- <TouchableOpacity
1021
- style={styles.overlay}
1022
- activeOpacity={1}
1023
- onPress={() => setShowOverlay(false)}
1024
- >
1025
- <View style={styles.overlayContent}>
1026
- <Text style={styles.overlayText}>{message}</Text>
1027
- <Button title="Close" onPress={() => setShowOverlay(false)} />
1028
- </View>
1029
- </TouchableOpacity>
1030
- ) : null,
1031
- disable: !showOverlay,
1032
- });
1033
-
1034
- const showCustomOverlay = (msg: string) => {
1035
- setMessage(msg);
1036
- setShowOverlay(true);
1037
- };
1038
-
1039
- return (
1040
- <View style={styles.container}>
1041
- <Button
1042
- title="Show Portal Overlay"
1043
- onPress={() => showCustomOverlay('This is rendered in the portal!')}
1044
- />
1045
- </View>
1046
- );
1047
- }
1048
-
1049
- const styles = StyleSheet.create({
1050
- container: {
1051
- flex: 1,
1052
- justifyContent: 'center',
1053
- alignItems: 'center',
1054
- },
1055
- overlay: {
1056
- ...StyleSheet.absoluteFillObject,
1057
- backgroundColor: 'rgba(0, 0, 0, 0.5)',
1058
- justifyContent: 'center',
1059
- alignItems: 'center',
1060
- },
1061
- overlayContent: {
1062
- backgroundColor: 'white',
1063
- padding: 30,
1064
- borderRadius: 10,
1065
- alignItems: 'center',
1066
- },
1067
- overlayText: {
1068
- fontSize: 18,
1069
- marginBottom: 20,
1070
- },
1071
- });
1072
- ```
1073
-
1074
- ### Multi-Level Portal Example
1075
-
1076
- ```tsx
1077
- import React, { createContext, useState } from 'react';
1078
- import { View, Button } from 'react-native';
1079
- import {
1080
- BottomSheetPortalProvider,
1081
- BottomSheetModal,
1082
- } from '@shaquillehinds/react-native-bottom-sheet';
1083
- import type { PortalContextValue } from '@shaquillehinds/react-native-bottom-sheet';
1084
-
1085
- // Create custom portal contexts for different levels
1086
- const ScreenPortalContext = createContext<PortalContextValue | undefined>(
1087
- undefined
1088
- );
1089
- const DialogPortalContext = createContext<PortalContextValue | undefined>(
1090
- undefined
1091
- );
1092
-
1093
- export default function App() {
1094
- return (
1095
- // Global portal for app-wide modals
1096
- <BottomSheetPortalProvider>
1097
- <Navigation />
1098
- </BottomSheetPortalProvider>
741
+ <SmoothBottomModal
742
+ showModal={props.showModal}
743
+ setShowModal={(value: boolean) => {
744
+ Keyboard.dismiss();
745
+ props.setShowModal(value);
746
+ }}
747
+ backgroundColor={colors.background[mode]}
748
+ showContentDelay={{ timeInMilliSecs: 250, type: 'mount' }}
749
+ snapPoints={[85]}
750
+ >
751
+ <ChatAppsModalContent {...props} mode={mode} colors={colors} />
752
+ </SmoothBottomModal>
1099
753
  );
1100
754
  }
1101
755
 
1102
- function MyScreen() {
1103
- const [showScreenModal, setShowScreenModal] = useState(false);
1104
- const [showDialogModal, setShowDialogModal] = useState(false);
756
+ function ChatAppsModalContent({ setShowModal, colors, mode }: ContentProps) {
757
+ const apps = [
758
+ {
759
+ id: 'flashcards',
760
+ title: 'Flashcards',
761
+ onPress: () => {
762
+ setShowModal(false);
763
+ router.push('/platform/practice/flashcards');
764
+ },
765
+ },
766
+ // ...
767
+ ];
1105
768
 
1106
- return (
1107
- // Screen-specific portal
1108
- <BottomSheetPortalProvider CustomPortalContext={ScreenPortalContext}>
1109
- <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
1110
- <Button
1111
- title="Open Screen Modal"
1112
- onPress={() => setShowScreenModal(true)}
1113
- />
1114
-
1115
- {/* Modal using screen portal */}
1116
- <BottomSheetModal
1117
- CustomPortalContext={ScreenPortalContext}
1118
- showModal={showScreenModal}
1119
- setShowModal={setShowScreenModal}
1120
- snapPoints={[50]}
1121
- >
1122
- <View style={{ padding: 20 }}>
1123
- <Text>Screen-level modal</Text>
1124
- <Button
1125
- title="Open Dialog"
1126
- onPress={() => setShowDialogModal(true)}
1127
- />
1128
-
1129
- {/* Nested modal using dialog portal */}
1130
- <BottomSheetPortalProvider
1131
- CustomPortalContext={DialogPortalContext}
1132
- >
1133
- <BottomSheetModal
1134
- CustomPortalContext={DialogPortalContext}
1135
- showModal={showDialogModal}
1136
- setShowModal={setShowDialogModal}
1137
- snapPoints={[30]}
1138
- >
1139
- <View style={{ padding: 20 }}>
1140
- <Text>Dialog-level modal</Text>
1141
- </View>
1142
- </BottomSheetModal>
1143
- </BottomSheetPortalProvider>
1144
- </View>
1145
- </BottomSheetModal>
1146
- </View>
1147
- </BottomSheetPortalProvider>
1148
- );
769
+ return <Layout padding={[2, 4]}>{/* render apps */}</Layout>;
1149
770
  }
1150
771
  ```
1151
772
 
1152
- ## Performance Best Practices
1153
-
1154
- 1. **Use `useImperativeHandle` pattern**: This package follows the render isolation pattern to prevent parent re-renders when animating
1155
-
1156
- 2. **Optimize heavy content**: Use `showContentDelay` with `type: 'mount'` for complex UIs:
1157
-
1158
- ```tsx
1159
- <BottomSheetModal
1160
- showContentDelay={{ type: 'mount', timeInMilliSecs: 100 }}
1161
- contentContainerStyle={{ minHeight: 400 }}
1162
- >
1163
- <ComplexComponent />
1164
- </BottomSheetModal>
1165
- ```
1166
-
1167
- 3. **Memoize callbacks**: Use `useCallback` for callbacks to prevent unnecessary re-renders:
1168
-
1169
- ```tsx
1170
- const handleSnapPoint = useCallback((index: number) => {
1171
- console.log('Snap point:', index);
1172
- }, []);
1173
- ```
1174
-
1175
- 4. **Keep snap points stable**: Define snap points outside the component or use `useMemo`:
1176
- ```tsx
1177
- const snapPoints = useMemo(() => [30, 60, 90], []);
1178
- ```
1179
-
1180
- ## Common Patterns
1181
-
1182
- ### Confirmation Dialog
773
+ ### Global sheet driven from a store
1183
774
 
1184
775
  ```tsx
1185
- function ConfirmDialog() {
1186
- const [show, setShow] = useState(false);
776
+ export default function UploadMaterialModal() {
777
+ const ref = useRef<BottomModalRef>(null);
778
+ const setUploadModalRef = useApp((state) => state.setUploadModalRef);
779
+ useEffect(() => {
780
+ setUploadModalRef(ref);
781
+ }, [ref.current]);
782
+ const { colors, mode } = useTheme();
1187
783
 
1188
784
  return (
1189
- <BottomSheetModal
1190
- showModal={show}
1191
- setShowModal={setShow}
1192
- snapPoints={[30]}
1193
- contentContainerStyle={{ padding: 20 }}
785
+ <SmoothBottomModal
786
+ ref={ref}
787
+ disablePortal
788
+ dragArea="full"
789
+ snapPoints={[90]}
790
+ backgroundColor={colors.background[mode]}
1194
791
  >
1195
- <Text style={{ fontSize: 18, marginBottom: 20 }}>Are you sure?</Text>
1196
- <View style={{ flexDirection: 'row', gap: 10 }}>
1197
- <Button title="Cancel" onPress={() => setShow(false)} />
1198
- <Button title="Confirm" onPress={handleConfirm} />
1199
- </View>
1200
- </BottomSheetModal>
792
+ <UploadMaterialModalContent />
793
+ </SmoothBottomModal>
1201
794
  );
1202
795
  }
1203
796
  ```
1204
797
 
1205
- ### Selection List
798
+ Mount it once near the app root. Anything can open it via the stored ref.
799
+
800
+ ### Selection sheet with state owned by the screen
1206
801
 
1207
802
  ```tsx
1208
- function SelectionList() {
1209
- const [show, setShow] = useState(false);
803
+ function ChooseFlashcardsContent(props: ChooseFlashcardsProps) {
804
+ const t = useTranslation();
1210
805
 
1211
806
  return (
1212
- <BottomSheetModal
1213
- showModal={show}
1214
- setShowModal={setShow}
1215
- snapPoints={[50, 80]}
1216
- >
1217
- <BottomSheetFlatlist
1218
- data={options}
1219
- renderItem={({ item }) => (
1220
- <TouchableOpacity onPress={() => handleSelect(item)}>
1221
- <Text style={{ padding: 15 }}>{item.label}</Text>
1222
- </TouchableOpacity>
1223
- )}
807
+ <>
808
+ <SelectFlashcardSetsList
809
+ selectedFlashcardSets={props.selectedFlashcards}
810
+ setSelectedFlashcardSets={props.setSelectedFlashcards}
811
+ searchBar
1224
812
  />
1225
- </BottomSheetModal>
813
+ <PrimaryButton
814
+ disabled={props.selectedFlashcards.length === 0}
815
+ onPress={() => {
816
+ props.onFlashcardSelect(props.selectedFlashcards);
817
+ props.setToggled(false);
818
+ }}
819
+ >
820
+ {t('confirm')}
821
+ </PrimaryButton>
822
+ </>
1226
823
  );
1227
824
  }
1228
825
  ```
1229
826
 
1230
- ### Filter Panel
827
+ Selection survives close and reopen because the screen holds it.
1231
828
 
1232
- ```tsx
1233
- function FilterPanel() {
1234
- const [show, setShow] = useState(false);
1235
-
1236
- return (
1237
- <BottomSheet
1238
- keepMounted={true}
1239
- bottomOffset={100}
1240
- snapPoints={[10, 70]}
1241
- showModal={show}
1242
- setShowModal={setShow}
1243
- >
1244
- <BottomSheetScrollView>
1245
- <FilterOptions />
1246
- </BottomSheetScrollView>
1247
- </BottomSheet>
1248
- );
1249
- }
1250
- ```
1251
-
1252
- ### Toast Notification (Using Portal)
829
+ ### Closing before navigating
1253
830
 
1254
831
  ```tsx
1255
- import { useBottomSheetPortalComponent } from '@shaquillehinds/react-native-bottom-sheet';
1256
-
1257
- function useToast() {
1258
- const [message, setMessage] = useState('');
1259
- const [visible, setVisible] = useState(false);
1260
-
1261
- useBottomSheetPortalComponent({
1262
- name: 'toast',
1263
- Component: visible ? (
1264
- <Animated.View
1265
- entering={SlideInDown}
1266
- exiting={SlideOutUp}
1267
- style={{
1268
- position: 'absolute',
1269
- top: 50,
1270
- alignSelf: 'center',
1271
- backgroundColor: '#333',
1272
- padding: 15,
1273
- borderRadius: 8,
1274
- }}
1275
- >
1276
- <Text style={{ color: 'white' }}>{message}</Text>
1277
- </Animated.View>
1278
- ) : null,
1279
- disable: !visible,
1280
- });
1281
-
1282
- const show = (msg: string) => {
1283
- setMessage(msg);
1284
- setVisible(true);
1285
- setTimeout(() => setVisible(false), 3000);
1286
- };
1287
-
1288
- return { show };
1289
- }
1290
-
1291
- // Usage
1292
- function MyComponent() {
1293
- const toast = useToast();
1294
-
1295
- return <Button title="Show Toast" onPress={() => toast.show('Hello!')} />;
1296
- }
832
+ ref.current?.closeModal({
833
+ isNavigating: true,
834
+ onClose: () => navigation.navigate('NextScreen'),
835
+ });
1297
836
  ```
1298
837
 
1299
- ### Loading Overlay (Using Portal)
838
+ Or skip the animation entirely with `skipAnimation: true`.
1300
839
 
1301
- ```tsx
1302
- import { useBottomSheetPortalComponent } from '@shaquillehinds/react-native-bottom-sheet';
1303
-
1304
- function useLoadingOverlay() {
1305
- const [isLoading, setIsLoading] = useState(false);
1306
-
1307
- useBottomSheetPortalComponent({
1308
- name: 'loading-overlay',
1309
- Component: isLoading ? (
1310
- <View
1311
- style={{
1312
- ...StyleSheet.absoluteFillObject,
1313
- backgroundColor: 'rgba(0, 0, 0, 0.5)',
1314
- justifyContent: 'center',
1315
- alignItems: 'center',
1316
- }}
1317
- >
1318
- <ActivityIndicator size="large" color="#fff" />
1319
- </View>
1320
- ) : null,
1321
- disable: !isLoading,
1322
- });
1323
-
1324
- return { setIsLoading };
1325
- }
1326
-
1327
- // Usage
1328
- function MyComponent() {
1329
- const { setIsLoading } = useLoadingOverlay();
1330
-
1331
- const handleSubmit = async () => {
1332
- setIsLoading(true);
1333
- await api.submit();
1334
- setIsLoading(false);
1335
- };
1336
-
1337
- return <Button title="Submit" onPress={handleSubmit} />;
1338
- }
1339
- ```
840
+ ---
1340
841
 
1341
842
  ## Troubleshooting
1342
843
 
1343
- ### Modal doesn't appear
844
+ **Sheet does not appear.** Check the Reanimated Babel plugin is installed and last
845
+ in the list, and that `BottomSheetPortalProvider` is mounted at the app root.
1344
846
 
1345
- Ensure you've set up React Native Reanimated and Gesture Handler properly:
847
+ **Sheet renders behind navigation.** The provider is too low in the tree. It must
848
+ wrap `NavigationContainer`, not sit inside it.
1346
849
 
1347
- ```tsx
1348
- // babel.config.js
1349
- module.exports = {
1350
- presets: ['module:metro-react-native-babel-preset'],
1351
- plugins: ['react-native-reanimated/plugin'],
1352
- };
1353
- ```
1354
-
1355
- **Also verify `BottomSheetPortalProvider` is set up at your app root:**
850
+ **Sheet opens collapsed or very short.** Using `showContentDelay: { type: 'mount' }`
851
+ without `snapPoints` or a `minHeight` on `contentContainerStyle`. There is nothing
852
+ to measure at open time.
1356
853
 
1357
- ```tsx
1358
- // App.tsx
1359
- import { BottomSheetPortalProvider } from '@shaquillehinds/react-native-bottom-sheet';
854
+ **Scrolling and dragging fight each other.** Swap the bare `FlatList` / `ScrollView`
855
+ for `BottomSheetFlatlist` / `BottomSheetScrollView`, and drop `dragArea="full"`.
1360
856
 
1361
- export default function App() {
1362
- return (
1363
- <BottomSheetPortalProvider>
1364
- <YourNavigator />
1365
- </BottomSheetPortalProvider>
1366
- );
1367
- }
1368
- ```
857
+ **Drag down still dismisses despite `keepMounted`.** `keepMounted` has no effect
858
+ without `snapPoints`.
1369
859
 
1370
- ### Modal appears behind navigation or other elements
860
+ **Sheet cannot be dragged.** Either `dragArea="none"`, or `hideBumper` combined
861
+ with the default `dragArea="bumper"`, or the keyboard is open — dragging is
862
+ disabled while it is, unless `allowDragWhileKeyboardVisible` is set.
1371
863
 
1372
- This typically means the portal provider is not at a high enough level in your component tree. The provider should wrap your navigation container:
864
+ **Content flickers or unmounts early.** Increase `unMountBufferTimeMS` on the
865
+ provider.
1373
866
 
1374
- ```tsx
1375
- // Correct - Provider wraps navigation
1376
- <BottomSheetPortalProvider>
1377
- <NavigationContainer>
1378
- <Stack.Navigator>
1379
- {/* screens */}
1380
- </Stack.Navigator>
1381
- </NavigationContainer>
1382
- </BottomSheetPortalProvider>
867
+ **Sheet is slow to open.** Move state into a `*Content` component and add
868
+ `showContentDelay={{ type: 'mount', timeInMilliSecs: 250 }}`.
1383
869
 
1384
- // Incorrect - Provider inside navigation
1385
- <NavigationContainer>
1386
- <BottomSheetPortalProvider>
1387
- <Stack.Navigator>
1388
- {/* screens */}
1389
- </Stack.Navigator>
1390
- </BottomSheetPortalProvider>
1391
- </NavigationContainer>
1392
- ```
870
+ **Screen feels sluggish even with sheets closed.** Hooks are sitting in a sheet
871
+ wrapper's function body. Move them into the content component.
1393
872
 
1394
- ### Portal content flickers or unmounts unexpectedly
873
+ ---
1395
874
 
1396
- Adjust the `unMountBufferTimeMS` prop on the provider:
875
+ ## AI agent rules
1397
876
 
1398
- ```tsx
1399
- <BottomSheetPortalProvider unMountBufferTimeMS={200}>
1400
- <YourApp />
1401
- </BottomSheetPortalProvider>
1402
- ```
877
+ The package ships a rules file written for AI coding agents (Claude Code, Cursor,
878
+ Codex, Copilot, etc.) at `rules/AGENT_RULES.md`. It tells an agent to keep sheet
879
+ state in a separate content component so nothing loads until the sheet is opened,
880
+ to reach for `BottomSheetFlatlist` / `BottomSheetScrollView` instead of raw
881
+ scrollables, and it lists every prop and ref method so it cannot invent APIs or
882
+ fall back to patterns from a different bottom sheet library. Point your agent at
883
+ it with any of the following.
1403
884
 
1404
- ### Scrolling issues
885
+ **Copy it into your project (recommended)**
1405
886
 
1406
- Always use `BottomSheetFlatlist` or `BottomSheetScrollView` for scrollable content inside the bottom sheet.
887
+ ```sh
888
+ npx rnbs-rules # writes ./AGENTS.md
889
+ npx rnbs-rules cursor # writes ./.cursor/rules/react-native-bottom-sheet.mdc (alwaysApply)
890
+ npx rnbs-rules claude # writes ./.claude/rules/react-native-bottom-sheet.md
891
+ npx rnbs-rules codex # writes ./.codex/rules/react-native-bottom-sheet.md
892
+ npx rnbs-rules copilot # writes ./.github/instructions/react-native-bottom-sheet.instructions.md
893
+ npx rnbs-rules windsurf # writes ./.windsurf/rules/react-native-bottom-sheet.md
894
+ npx rnbs-rules docs/ai/bottom-sheet.md # custom path
895
+ ```
1407
896
 
1408
- ### Keyboard issues
897
+ Add `--force` to overwrite an existing file. `--print` writes the rules to stdout
898
+ instead of to disk. Re-run after upgrading the package to pick up rule changes.
1409
899
 
1410
- Use `avoidKeyboard` or `inputsForKeyboardToAvoid` props and ensure inputs have proper refs.
900
+ **Reference it without copying (Claude Code)**
1411
901
 
1412
- ### Android navigation glitches
902
+ `CLAUDE.md` supports `@path` imports, so a single line keeps the rules in sync with
903
+ the installed version:
1413
904
 
1414
- Use `isNavigating` or `skipAnimation` when closing before navigation:
905
+ ```md
906
+ # CLAUDE.md
1415
907
 
1416
- ```tsx
1417
- bottomSheetRef.current?.closeModal({
1418
- isNavigating: true,
1419
- onClose: () => navigation.navigate('NextScreen'),
1420
- });
908
+ @node_modules/@shaquillehinds/react-native-bottom-sheet/rules/AGENT_RULES.md
1421
909
  ```
1422
910
 
1423
- ## Dependencies
1424
-
1425
- - `react-native-reanimated` ^3.0.0
1426
- - `react-native-gesture-handler` ^2.0.0
1427
- - `@shaquillehinds/react-native-essentials` ^1.8.0
911
+ **Reference it from a generic `AGENTS.md`**
1428
912
 
1429
- ## Contributing
913
+ ```md
914
+ Before writing any bottom sheet, read and follow
915
+ node_modules/@shaquillehinds/react-native-bottom-sheet/rules/AGENT_RULES.md.
916
+ ```
1430
917
 
1431
- Contributions are welcome! Please read the [Contributing Guide](CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests.
918
+ ---
1432
919
 
1433
920
  ## License
1434
921
 
1435
922
  MIT © [Shaquille Hinds](https://github.com/shaquillehinds)
1436
-
1437
- ## Author
1438
-
1439
- **Shaquille Hinds**
1440
-
1441
- - GitHub: [@shaquillehinds](https://github.com/shaquillehinds)
1442
- - Email: shaqdulove@gmail.com
1443
-
1444
- ## Related Packages
1445
-
1446
- - [@shaquillehinds/react-native-essentials](https://www.npmjs.com/package/@shaquillehinds/react-native-essentials) - Essential utilities and components for React Native
1447
-
1448
- ## Changelog
1449
-
1450
- See [Releases](https://github.com/shaquillehinds/react-native-bottom-sheet/releases) for version history.
1451
-
1452
- ## Support
1453
-
1454
- If you encounter any issues or have questions:
1455
-
1456
- - Open an issue on [GitHub](https://github.com/shaquillehinds/react-native-bottom-sheet/issues)
1457
- - Check existing issues for solutions
1458
- - Review the examples in this README
1459
-
1460
- ## Acknowledgments
1461
-
1462
- Built with React Native Reanimated and Gesture Handler for optimal performance and smooth interactions.