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