insyd-bottom-sheet 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Ishan Gupta (https://github.com/ishan-crd)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,280 @@
1
+ # insyd-bottom-sheet
2
+
3
+ [![npm version](https://img.shields.io/npm/v/insyd-bottom-sheet.svg?style=flat-square)](https://www.npmjs.com/package/insyd-bottom-sheet)
4
+ [![license](https://img.shields.io/npm/l/insyd-bottom-sheet.svg?style=flat-square)](./LICENSE)
5
+ [![platform](https://img.shields.io/badge/platform-iOS%20%7C%20Android-lightgrey?style=flat-square)](https://reactnative.dev)
6
+ [![expo compatible](https://img.shields.io/badge/Expo%20Go-compatible-4630EB?style=flat-square&logo=expo)](https://expo.dev)
7
+
8
+ A smooth, physics-based bottom sheet for React Native — built by [Ishan Gupta](https://github.com/ishan-crd).
9
+
10
+ Powered by `react-native-reanimated` and `react-native-gesture-handler`. **Works in Expo Go, Expo bare workflow, and React Native CLI** — no native rebuild required.
11
+
12
+ ---
13
+
14
+ ## Features
15
+
16
+ - **Spring physics** — feels natural, not mechanical
17
+ - **Drag-to-dismiss** — with configurable distance and velocity thresholds
18
+ - **Backdrop tap to dismiss**
19
+ - **Keyboard-avoiding** — content shifts up when the keyboard appears (iOS padding / Android height)
20
+ - **Fully typed** — complete TypeScript support with exported interfaces
21
+ - **No native code** — runs in Expo Go out of the box
22
+ - **Zero extra dependencies** — only React Native, Reanimated 3, and Gesture Handler
23
+
24
+ ---
25
+
26
+ ## Installation
27
+
28
+ ```sh
29
+ # npm
30
+ npm install insyd-bottom-sheet
31
+
32
+ # yarn
33
+ yarn add insyd-bottom-sheet
34
+
35
+ # Expo (recommended — picks compatible peer dep versions automatically)
36
+ npx expo install insyd-bottom-sheet
37
+ ```
38
+
39
+ ### Peer dependencies
40
+
41
+ This package requires `react-native-reanimated` and `react-native-gesture-handler`. If you haven't already installed them:
42
+
43
+ ```sh
44
+ npx expo install react-native-reanimated react-native-gesture-handler
45
+ ```
46
+
47
+ For **bare React Native**, follow the official setup guides:
48
+ - [Reanimated installation](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/getting-started/)
49
+ - [Gesture Handler installation](https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/installation/)
50
+
51
+ > **Expo Go users:** both packages ship with Expo Go — no custom dev client needed.
52
+
53
+ ---
54
+
55
+ ## Setup
56
+
57
+ ### 1. Wrap your app in `GestureHandlerRootView`
58
+
59
+ This is required by `react-native-gesture-handler`. Wrap your root component once:
60
+
61
+ ```tsx
62
+ // App.tsx (or _layout.tsx for Expo Router)
63
+ import { GestureHandlerRootView } from "react-native-gesture-handler";
64
+
65
+ export default function App() {
66
+ return (
67
+ <GestureHandlerRootView style={{ flex: 1 }}>
68
+ {/* rest of your app */}
69
+ </GestureHandlerRootView>
70
+ );
71
+ }
72
+ ```
73
+
74
+ ### 2. Add the Reanimated Babel plugin
75
+
76
+ In your `babel.config.js`:
77
+
78
+ ```js
79
+ module.exports = {
80
+ presets: ["babel-preset-expo"], // or 'metro-react-native-babel-preset' for RN CLI
81
+ plugins: ["react-native-reanimated/plugin"],
82
+ };
83
+ ```
84
+
85
+ After editing `babel.config.js`, restart Metro with a cleared cache:
86
+
87
+ ```sh
88
+ npx expo start --clear
89
+ # or for RN CLI
90
+ npx react-native start --reset-cache
91
+ ```
92
+
93
+ ---
94
+
95
+ ## Usage
96
+
97
+ ### Basic
98
+
99
+ ```tsx
100
+ import React, { useState } from "react";
101
+ import { Button, Text, View } from "react-native";
102
+ import { GestureHandlerRootView } from "react-native-gesture-handler";
103
+ import { SmoothSheet } from "insyd-bottom-sheet";
104
+
105
+ export default function App() {
106
+ const [open, setOpen] = useState(false);
107
+
108
+ return (
109
+ <GestureHandlerRootView style={{ flex: 1 }}>
110
+ <View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
111
+ <Button title="Open Sheet" onPress={() => setOpen(true)} />
112
+ </View>
113
+
114
+ <SmoothSheet isVisible={open} onDismiss={() => setOpen(false)}>
115
+ <View style={{ padding: 24 }}>
116
+ <Text style={{ fontSize: 20, fontWeight: "600" }}>Hello 👋</Text>
117
+ <Text style={{ marginTop: 8, color: "#64748b" }}>
118
+ Drag down or tap the backdrop to dismiss.
119
+ </Text>
120
+ </View>
121
+ </SmoothSheet>
122
+ </GestureHandlerRootView>
123
+ );
124
+ }
125
+ ```
126
+
127
+ ### Custom height
128
+
129
+ ```tsx
130
+ <SmoothSheet
131
+ isVisible={open}
132
+ onDismiss={() => setOpen(false)}
133
+ minHeightFraction={0.5} // sheet takes up at least 50% of screen
134
+ maxHeightFraction={0.85} // caps at 85% of screen
135
+ >
136
+ {/* your content */}
137
+ </SmoothSheet>
138
+ ```
139
+
140
+ ### Custom appearance
141
+
142
+ ```tsx
143
+ <SmoothSheet
144
+ isVisible={open}
145
+ onDismiss={() => setOpen(false)}
146
+ backgroundColor="#1e293b"
147
+ borderRadius={20}
148
+ handleColor="#475569"
149
+ backdropColor="rgba(0,0,0,0.7)"
150
+ >
151
+ {/* dark-themed content */}
152
+ </SmoothSheet>
153
+ ```
154
+
155
+ ### Custom spring physics
156
+
157
+ ```tsx
158
+ <SmoothSheet
159
+ isVisible={open}
160
+ onDismiss={() => setOpen(false)}
161
+ springConfig={{ damping: 20, stiffness: 220, mass: 0.8 }}
162
+ >
163
+ {/* snappier feel */}
164
+ </SmoothSheet>
165
+ ```
166
+
167
+ ### With a ScrollView inside
168
+
169
+ Wrap long content in a `ScrollView`. The sheet handles keyboard avoidance automatically:
170
+
171
+ ```tsx
172
+ import { ScrollView, Text } from "react-native";
173
+ import { SmoothSheet } from "insyd-bottom-sheet";
174
+
175
+ <SmoothSheet
176
+ isVisible={open}
177
+ onDismiss={() => setOpen(false)}
178
+ minHeightFraction={0.7}
179
+ >
180
+ <ScrollView
181
+ contentContainerStyle={{ padding: 24, paddingBottom: 48 }}
182
+ showsVerticalScrollIndicator={false}
183
+ keyboardShouldPersistTaps="handled"
184
+ >
185
+ {/* long content */}
186
+ </ScrollView>
187
+ </SmoothSheet>
188
+ ```
189
+
190
+ ### With TextInput (keyboard-aware)
191
+
192
+ The built-in `KeyboardAvoidingView` handles this automatically:
193
+
194
+ ```tsx
195
+ import { TextInput, View } from "react-native";
196
+ import { SmoothSheet } from "insyd-bottom-sheet";
197
+
198
+ <SmoothSheet isVisible={open} onDismiss={() => setOpen(false)}>
199
+ <View style={{ padding: 24, gap: 12 }}>
200
+ <TextInput
201
+ placeholder="Your name"
202
+ style={{
203
+ borderWidth: 1,
204
+ borderColor: "#e2e8f0",
205
+ borderRadius: 8,
206
+ padding: 12,
207
+ }}
208
+ />
209
+ </View>
210
+ </SmoothSheet>
211
+ ```
212
+
213
+ ---
214
+
215
+ ## Props
216
+
217
+ | Prop | Type | Default | Required | Description |
218
+ |------|------|---------|----------|-------------|
219
+ | `isVisible` | `boolean` | — | ✅ | Controls sheet visibility |
220
+ | `onDismiss` | `() => void` | — | ✅ | Called when sheet should close |
221
+ | `children` | `React.ReactNode` | — | ✅ | Content rendered inside the sheet |
222
+ | `minHeightFraction` | `number` | `0.4` | | Min height as a fraction of screen (0–1) |
223
+ | `maxHeightFraction` | `number` | `0.97` | | Max height as a fraction of screen (0–1) |
224
+ | `backgroundColor` | `string` | `"#ffffff"` | | Sheet background color |
225
+ | `borderRadius` | `number` | `28` | | Top corner radius |
226
+ | `handleColor` | `string` | `"#CBD5E1"` | | Drag handle pill color |
227
+ | `backdropColor` | `string` | `"rgba(0,0,0,0.5)"` | | Backdrop overlay color |
228
+ | `dismissThreshold` | `number` | `80` | | Drag distance in px to trigger dismiss |
229
+ | `dismissVelocityThreshold` | `number` | `800` | | Drag velocity in px/s to trigger dismiss |
230
+ | `springConfig` | `SpringConfig` | see below | | Override spring animation |
231
+
232
+ ### SpringConfig
233
+
234
+ | Key | Type | Default | Description |
235
+ |-----|------|---------|-------------|
236
+ | `damping` | `number` | `28` | Higher = less bouncy |
237
+ | `stiffness` | `number` | `160` | Higher = faster |
238
+ | `mass` | `number` | `0.9` | Higher = heavier feel |
239
+
240
+ ---
241
+
242
+ ## TypeScript
243
+
244
+ Both the component and its props are fully typed. You can import the types:
245
+
246
+ ```ts
247
+ import type { SmoothSheetProps, SpringConfig } from "insyd-bottom-sheet";
248
+ ```
249
+
250
+ ---
251
+
252
+ ## Requirements
253
+
254
+ | | Minimum version |
255
+ |---|---|
256
+ | React Native | 0.71 |
257
+ | React | 18.0 |
258
+ | `react-native-reanimated` | 3.0.0 |
259
+ | `react-native-gesture-handler` | 2.0.0 |
260
+ | Expo SDK *(if using Expo)* | 49+ |
261
+
262
+ ---
263
+
264
+ ## How it works
265
+
266
+ `insyd-bottom-sheet` uses `useSharedValue` and `withSpring` from Reanimated to animate the sheet on the UI thread — meaning animations are never blocked by JavaScript work. The drag-to-dismiss gesture runs via `Gesture.Pan()` from Gesture Handler, also on the UI thread. No bridge round-trips, no jank.
267
+
268
+ ---
269
+
270
+ ## License
271
+
272
+ MIT © 2024 [Ishan Gupta](https://github.com/ishan-crd)
273
+
274
+ ---
275
+
276
+ ## Contributing
277
+
278
+ Issues and PRs welcome at [github.com/ishan-crd/insyd-bottom-sheet](https://github.com/ishan-crd/insyd-bottom-sheet).
279
+
280
+ For bug reports, questions, or anything else — reach out at [ishan@insyd.in](mailto:ishan@insyd.in).
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "insyd-bottom-sheet",
3
+ "version": "0.1.0",
4
+ "description": "A smooth, physics-based bottom sheet for React Native — by Ishan Gupta. Works with Expo Go, bare workflow, and React Native CLI.",
5
+ "main": "src/index.ts",
6
+ "types": "src/index.ts",
7
+ "files": [
8
+ "src",
9
+ "LICENSE",
10
+ "README.md"
11
+ ],
12
+ "scripts": {
13
+ "typecheck": "tsc --noEmit"
14
+ },
15
+ "keywords": [
16
+ "react-native",
17
+ "bottom-sheet",
18
+ "reanimated",
19
+ "gesture-handler",
20
+ "expo",
21
+ "expo-go",
22
+ "modal",
23
+ "sheet",
24
+ "animated",
25
+ "insyd",
26
+ "smooth"
27
+ ],
28
+ "author": {
29
+ "name": "Ishan Gupta",
30
+ "url": "https://github.com/ishan-crd"
31
+ },
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/ishan-crd/insyd-bottom-sheet.git"
36
+ },
37
+ "bugs": {
38
+ "url": "https://github.com/ishan-crd/insyd-bottom-sheet/issues"
39
+ },
40
+ "homepage": "https://github.com/ishan-crd/insyd-bottom-sheet#readme",
41
+ "peerDependencies": {
42
+ "react": ">=18.0.0",
43
+ "react-native": ">=0.71.0",
44
+ "react-native-gesture-handler": ">=2.0.0",
45
+ "react-native-reanimated": ">=3.0.0"
46
+ },
47
+ "devDependencies": {
48
+ "@types/react": "^18.2.0",
49
+ "@types/react-native": "^0.73.0",
50
+ "typescript": "^5.3.0"
51
+ }
52
+ }
@@ -0,0 +1,230 @@
1
+ import React, { useEffect } from "react";
2
+ import {
3
+ Dimensions,
4
+ KeyboardAvoidingView,
5
+ Platform,
6
+ Pressable,
7
+ StyleSheet,
8
+ View,
9
+ } from "react-native";
10
+ import {
11
+ Gesture,
12
+ GestureDetector,
13
+ } from "react-native-gesture-handler";
14
+ import Animated, {
15
+ runOnJS,
16
+ useAnimatedStyle,
17
+ useSharedValue,
18
+ withSpring,
19
+ withTiming,
20
+ } from "react-native-reanimated";
21
+
22
+ const SCREEN_HEIGHT = Dimensions.get("window").height;
23
+
24
+ const DEFAULT_SPRING = { damping: 28, stiffness: 160, mass: 0.9 };
25
+
26
+ export interface SpringConfig {
27
+ damping?: number;
28
+ stiffness?: number;
29
+ mass?: number;
30
+ }
31
+
32
+ export interface SmoothSheetProps {
33
+ /** Controls whether the sheet is visible. */
34
+ isVisible: boolean;
35
+ /** Called when the sheet should be dismissed (backdrop tap, drag-to-dismiss, or close button). */
36
+ onDismiss: () => void;
37
+ /** Content rendered inside the sheet. */
38
+ children: React.ReactNode;
39
+ /**
40
+ * Minimum height of the sheet as a fraction of screen height (0–1).
41
+ * @default 0.4
42
+ */
43
+ minHeightFraction?: number;
44
+ /**
45
+ * Maximum height of the sheet as a fraction of screen height (0–1).
46
+ * @default 0.97
47
+ */
48
+ maxHeightFraction?: number;
49
+ /**
50
+ * Background color of the sheet.
51
+ * @default "#ffffff"
52
+ */
53
+ backgroundColor?: string;
54
+ /**
55
+ * Border radius applied to the top-left and top-right corners.
56
+ * @default 28
57
+ */
58
+ borderRadius?: number;
59
+ /**
60
+ * Color of the drag handle pill.
61
+ * @default "#CBD5E1"
62
+ */
63
+ handleColor?: string;
64
+ /**
65
+ * Background color of the backdrop overlay.
66
+ * @default "rgba(0,0,0,0.5)"
67
+ */
68
+ backdropColor?: string;
69
+ /**
70
+ * Vertical drag distance in px required to trigger dismiss.
71
+ * @default 80
72
+ */
73
+ dismissThreshold?: number;
74
+ /**
75
+ * Vertical velocity in px/s that triggers dismiss even before reaching dismissThreshold.
76
+ * @default 800
77
+ */
78
+ dismissVelocityThreshold?: number;
79
+ /**
80
+ * Override the spring animation config.
81
+ * Merged over the default: { damping: 28, stiffness: 160, mass: 0.9 }
82
+ */
83
+ springConfig?: SpringConfig;
84
+ }
85
+
86
+ export function SmoothSheet({
87
+ isVisible,
88
+ onDismiss,
89
+ children,
90
+ minHeightFraction = 0.4,
91
+ maxHeightFraction = 0.97,
92
+ backgroundColor = "#ffffff",
93
+ borderRadius = 28,
94
+ handleColor = "#CBD5E1",
95
+ backdropColor = "rgba(0,0,0,0.5)",
96
+ dismissThreshold = 80,
97
+ dismissVelocityThreshold = 800,
98
+ springConfig,
99
+ }: SmoothSheetProps) {
100
+ const spring = { ...DEFAULT_SPRING, ...springConfig };
101
+
102
+ const translateY = useSharedValue(SCREEN_HEIGHT);
103
+ const backdropOpacity = useSharedValue(0);
104
+
105
+ useEffect(() => {
106
+ if (isVisible) {
107
+ translateY.value = withSpring(0, spring);
108
+ backdropOpacity.value = withTiming(1, { duration: 250 });
109
+ } else {
110
+ translateY.value = withSpring(SCREEN_HEIGHT, spring);
111
+ backdropOpacity.value = withTiming(0, { duration: 200 });
112
+ }
113
+ // eslint-disable-next-line react-hooks/exhaustive-deps
114
+ }, [isVisible]);
115
+
116
+ function dismiss() {
117
+ translateY.value = withSpring(SCREEN_HEIGHT, spring);
118
+ backdropOpacity.value = withTiming(
119
+ 0,
120
+ { duration: 200 },
121
+ () => {
122
+ runOnJS(onDismiss)();
123
+ },
124
+ );
125
+ }
126
+
127
+ const panGesture = Gesture.Pan()
128
+ .onUpdate((e) => {
129
+ if (e.translationY > 0) {
130
+ translateY.value = e.translationY;
131
+ }
132
+ })
133
+ .onEnd((e) => {
134
+ if (
135
+ e.translationY > dismissThreshold ||
136
+ e.velocityY > dismissVelocityThreshold
137
+ ) {
138
+ runOnJS(dismiss)();
139
+ } else {
140
+ translateY.value = withSpring(0, spring);
141
+ }
142
+ });
143
+
144
+ const sheetStyle = useAnimatedStyle(() => ({
145
+ transform: [{ translateY: translateY.value }],
146
+ }));
147
+
148
+ const backdropStyle = useAnimatedStyle(() => ({
149
+ opacity: backdropOpacity.value,
150
+ }));
151
+
152
+ if (!isVisible && translateY.value === SCREEN_HEIGHT) {
153
+ return null;
154
+ }
155
+
156
+ return (
157
+ <View style={StyleSheet.absoluteFill} pointerEvents="box-none">
158
+ {/* Backdrop */}
159
+ <Animated.View
160
+ style={[
161
+ StyleSheet.absoluteFill,
162
+ { backgroundColor: backdropColor },
163
+ backdropStyle,
164
+ ]}
165
+ pointerEvents={isVisible ? "auto" : "none"}
166
+ >
167
+ <Pressable style={StyleSheet.absoluteFill} onPress={dismiss} />
168
+ </Animated.View>
169
+
170
+ {/* Sheet */}
171
+ <Animated.View
172
+ style={[
173
+ styles.sheet,
174
+ {
175
+ borderTopLeftRadius: borderRadius,
176
+ borderTopRightRadius: borderRadius,
177
+ backgroundColor,
178
+ minHeight: SCREEN_HEIGHT * minHeightFraction,
179
+ maxHeight: SCREEN_HEIGHT * maxHeightFraction,
180
+ },
181
+ sheetStyle,
182
+ ]}
183
+ pointerEvents="box-none"
184
+ >
185
+ {/* Drag handle */}
186
+ <GestureDetector gesture={panGesture}>
187
+ <View style={styles.handleArea}>
188
+ <View
189
+ style={[styles.handle, { backgroundColor: handleColor }]}
190
+ />
191
+ </View>
192
+ </GestureDetector>
193
+
194
+ {/* Content */}
195
+ <KeyboardAvoidingView
196
+ behavior={Platform.OS === "ios" ? "padding" : "height"}
197
+ style={styles.content}
198
+ >
199
+ {children}
200
+ </KeyboardAvoidingView>
201
+ </Animated.View>
202
+ </View>
203
+ );
204
+ }
205
+
206
+ const styles = StyleSheet.create({
207
+ sheet: {
208
+ position: "absolute",
209
+ bottom: 0,
210
+ left: 0,
211
+ right: 0,
212
+ shadowColor: "#000",
213
+ shadowOffset: { width: 0, height: -4 },
214
+ shadowOpacity: 0.12,
215
+ shadowRadius: 16,
216
+ elevation: 24,
217
+ },
218
+ handleArea: {
219
+ alignItems: "center",
220
+ paddingVertical: 12,
221
+ },
222
+ handle: {
223
+ width: 36,
224
+ height: 4,
225
+ borderRadius: 2,
226
+ },
227
+ content: {
228
+ flex: 1,
229
+ },
230
+ });
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { SmoothSheet } from "./SmoothSheet";
2
+ export type { SmoothSheetProps, SpringConfig } from "./SmoothSheet";