@lodev09/react-native-true-sheet 3.2.2 → 3.3.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/lib/module/TrueSheet.js.map +1 -1
  2. package/lib/module/TrueSheet.web.js +333 -0
  3. package/lib/module/TrueSheet.web.js.map +1 -0
  4. package/lib/module/TrueSheetProvider.js +26 -0
  5. package/lib/module/TrueSheetProvider.js.map +1 -0
  6. package/lib/module/TrueSheetProvider.web.js +74 -0
  7. package/lib/module/TrueSheetProvider.web.js.map +1 -0
  8. package/lib/module/index.js +2 -1
  9. package/lib/module/index.js.map +1 -1
  10. package/lib/module/navigation/screen/ReanimatedTrueSheetScreen.js +1 -1
  11. package/lib/module/navigation/screen/ReanimatedTrueSheetScreen.js.map +1 -1
  12. package/lib/module/navigation/screen/TrueSheetScreen.js +1 -1
  13. package/lib/module/navigation/screen/TrueSheetScreen.js.map +1 -1
  14. package/lib/module/reanimated/ReanimatedTrueSheet.js +2 -2
  15. package/lib/module/reanimated/ReanimatedTrueSheet.js.map +1 -1
  16. package/lib/module/reanimated/ReanimatedTrueSheet.web.js +81 -0
  17. package/lib/module/reanimated/ReanimatedTrueSheet.web.js.map +1 -0
  18. package/lib/module/reanimated/index.js +2 -2
  19. package/lib/module/reanimated/index.js.map +1 -1
  20. package/lib/module/reanimated/useReanimatedPositionChangeHandler.web.js +21 -0
  21. package/lib/module/reanimated/useReanimatedPositionChangeHandler.web.js.map +1 -0
  22. package/lib/typescript/src/TrueSheet.d.ts +2 -2
  23. package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
  24. package/lib/typescript/src/TrueSheet.types.d.ts +44 -0
  25. package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
  26. package/lib/typescript/src/TrueSheet.web.d.ts +3 -0
  27. package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -0
  28. package/lib/typescript/src/TrueSheetProvider.d.ts +17 -0
  29. package/lib/typescript/src/TrueSheetProvider.d.ts.map +1 -0
  30. package/lib/typescript/src/TrueSheetProvider.web.d.ts +22 -0
  31. package/lib/typescript/src/TrueSheetProvider.web.d.ts.map +1 -0
  32. package/lib/typescript/src/index.d.ts +1 -0
  33. package/lib/typescript/src/index.d.ts.map +1 -1
  34. package/lib/typescript/src/reanimated/ReanimatedTrueSheet.web.d.ts +41 -0
  35. package/lib/typescript/src/reanimated/ReanimatedTrueSheet.web.d.ts.map +1 -0
  36. package/lib/typescript/src/reanimated/useReanimatedPositionChangeHandler.web.d.ts +16 -0
  37. package/lib/typescript/src/reanimated/useReanimatedPositionChangeHandler.web.d.ts.map +1 -0
  38. package/package.json +11 -4
  39. package/src/TrueSheet.tsx +5 -1
  40. package/src/TrueSheet.types.ts +46 -0
  41. package/src/TrueSheet.web.tsx +407 -0
  42. package/src/TrueSheetProvider.tsx +29 -0
  43. package/src/TrueSheetProvider.web.tsx +81 -0
  44. package/src/index.ts +1 -0
  45. package/src/reanimated/ReanimatedTrueSheet.web.tsx +78 -0
  46. package/src/reanimated/useReanimatedPositionChangeHandler.web.ts +32 -0
@@ -0,0 +1,407 @@
1
+ import {
2
+ createElement,
3
+ Fragment,
4
+ forwardRef,
5
+ isValidElement,
6
+ useCallback,
7
+ useContext,
8
+ useEffect,
9
+ useImperativeHandle,
10
+ useMemo,
11
+ useRef,
12
+ useState,
13
+ } from 'react';
14
+ import { View, StyleSheet, useWindowDimensions } from 'react-native';
15
+
16
+ import {
17
+ BottomSheetBackdrop,
18
+ type BottomSheetBackdropProps,
19
+ BottomSheetFooter,
20
+ type BottomSheetFooterProps,
21
+ BottomSheetHandle,
22
+ type BottomSheetHandleProps,
23
+ BottomSheetModal,
24
+ BottomSheetView,
25
+ type SNAP_POINT_TYPE,
26
+ } from '@gorhom/bottom-sheet';
27
+ import { useDerivedValue, useSharedValue } from 'react-native-reanimated';
28
+
29
+ import { BottomSheetContext } from './TrueSheetProvider.web';
30
+ import type {
31
+ TrueSheetProps,
32
+ TrueSheetRef,
33
+ DetentChangeEvent,
34
+ DidBlurEvent,
35
+ DidDismissEvent,
36
+ DidFocusEvent,
37
+ DidPresentEvent,
38
+ MountEvent,
39
+ PositionChangeEvent,
40
+ WillBlurEvent,
41
+ WillDismissEvent,
42
+ WillFocusEvent,
43
+ WillPresentEvent,
44
+ DragBeginEvent,
45
+ DragChangeEvent,
46
+ DragEndEvent,
47
+ } from './TrueSheet.types';
48
+
49
+ const DEFAULT_CORNER_RADIUS = 16;
50
+ const DEFAULT_GRABBER_COLOR = 'rgba(0, 0, 0, 0.3)';
51
+
52
+ const renderSlot = (slot: TrueSheetProps['header'] | TrueSheetProps['footer']) => {
53
+ if (!slot) return null;
54
+ if (isValidElement(slot)) return slot;
55
+ return createElement(slot);
56
+ };
57
+
58
+ export const TrueSheet = forwardRef<TrueSheetRef, TrueSheetProps>((props, ref) => {
59
+ const {
60
+ name,
61
+ detents = [0.5, 1],
62
+ dismissible = true,
63
+ draggable = true,
64
+ dimmed = true,
65
+ dimmedDetentIndex = 0,
66
+ children,
67
+ scrollable = false,
68
+ initialDetentIndex = -1,
69
+ backgroundColor = '#ffffff',
70
+ cornerRadius = DEFAULT_CORNER_RADIUS,
71
+ grabber = true,
72
+ grabberOptions,
73
+ maxHeight,
74
+ header,
75
+ footer,
76
+ onMount,
77
+ onWillPresent,
78
+ onDidPresent,
79
+ onWillDismiss,
80
+ onDidDismiss,
81
+ onDetentChange,
82
+ onPositionChange,
83
+ onDragBegin,
84
+ onDragChange,
85
+ onDragEnd,
86
+ onWillFocus,
87
+ onDidFocus,
88
+ onWillBlur,
89
+ onDidBlur,
90
+ style,
91
+ } = props;
92
+
93
+ const { height: windowHeight } = useWindowDimensions();
94
+ const bottomSheetContext = useContext(BottomSheetContext);
95
+ const modalRef = useRef<BottomSheetModal>(null);
96
+ const initialDetentIndexRef = useRef(initialDetentIndex);
97
+ const currentIndexRef = useRef(0);
98
+ const isPresenting = useRef(false);
99
+ const isDismissing = useRef(false);
100
+ const isMinimized = useRef(false);
101
+ const isDragging = useRef(false);
102
+
103
+ const animatedPosition = useSharedValue(windowHeight);
104
+ const animatedIndex = useSharedValue(0);
105
+
106
+ const [snapIndex, setSnapIndex] = useState(initialDetentIndex);
107
+ const [isMounted, setIsMounted] = useState(false);
108
+
109
+ useDerivedValue(() => {
110
+ onPositionChange?.({
111
+ nativeEvent: {
112
+ position: animatedPosition.value,
113
+ index: animatedIndex.value,
114
+ detent: detents[animatedIndex.value] ?? 0,
115
+ realtime: true,
116
+ },
117
+ } as PositionChangeEvent);
118
+ });
119
+
120
+ const hasAutoDetent = detents.includes('auto');
121
+
122
+ const containerHeight = maxHeight ?? windowHeight;
123
+ const snapPoints = useMemo(
124
+ () =>
125
+ detents
126
+ .filter((detent): detent is number => detent !== 'auto' && typeof detent === 'number')
127
+ .map((detent) => Math.min(1, Math.max(0.1, detent)) * containerHeight),
128
+ [detents, containerHeight]
129
+ );
130
+
131
+ const handleChange = useCallback(
132
+ (index: number, _position: number, _type: SNAP_POINT_TYPE) => {
133
+ const previousIndex = currentIndexRef.current;
134
+ currentIndexRef.current = index;
135
+
136
+ // Handle drag end
137
+ if (isDragging.current && !isPresenting.current) {
138
+ isDragging.current = false;
139
+ onDragEnd?.({
140
+ nativeEvent: {
141
+ index,
142
+ position: animatedPosition.value,
143
+ detent: detents[index] ?? 0,
144
+ },
145
+ } as DragEndEvent);
146
+ }
147
+
148
+ if (!isPresenting.current && !isMinimized.current && previousIndex !== index && index >= 0) {
149
+ onDetentChange?.({
150
+ nativeEvent: {
151
+ index,
152
+ position: animatedPosition.value,
153
+ detent: detents[index] ?? 0,
154
+ },
155
+ } as DetentChangeEvent);
156
+ }
157
+
158
+ if (isPresenting.current) {
159
+ isPresenting.current = false;
160
+
161
+ onDidPresent?.({
162
+ nativeEvent: {
163
+ index,
164
+ position: animatedPosition.value,
165
+ detent: detents[index] ?? 0,
166
+ },
167
+ } as DidPresentEvent);
168
+
169
+ onDidFocus?.({ nativeEvent: null } as DidFocusEvent);
170
+ }
171
+
172
+ // Fire onDidBlur when sheet reaches minimized state (index -1 but still mounted)
173
+ if (isMinimized.current && index === -1) {
174
+ onDidBlur?.({ nativeEvent: null } as DidBlurEvent);
175
+ }
176
+
177
+ // Fire onDidFocus when sheet is restored from minimized state
178
+ if (isMinimized.current && index >= 0) {
179
+ isMinimized.current = false;
180
+ onDidFocus?.({ nativeEvent: null } as DidFocusEvent);
181
+ }
182
+ },
183
+ [detents, animatedPosition]
184
+ );
185
+
186
+ const handleDismiss = useCallback(() => {
187
+ onDidDismiss?.({ nativeEvent: null } as DidDismissEvent);
188
+
189
+ // Reset states since sheet is being dismissed
190
+ isMinimized.current = false;
191
+ isDismissing.current = false;
192
+ isDragging.current = false;
193
+ }, []);
194
+
195
+ const handleAnimate = useCallback(
196
+ (_fromIndex: number, toIndex: number) => {
197
+ // Detect drag begin (when not presenting or dismissing)
198
+ if (!isPresenting.current && !isDismissing.current && !isDragging.current && toIndex >= 0) {
199
+ isDragging.current = true;
200
+ onDragBegin?.({
201
+ nativeEvent: {
202
+ index: currentIndexRef.current,
203
+ position: animatedPosition.value,
204
+ detent: detents[currentIndexRef.current] ?? 0,
205
+ },
206
+ } as DragBeginEvent);
207
+ }
208
+
209
+ // Drag change during animation
210
+ if (isDragging.current && toIndex >= 0) {
211
+ onDragChange?.({
212
+ nativeEvent: {
213
+ index: toIndex,
214
+ position: animatedPosition.value,
215
+ detent: detents[toIndex] ?? 0,
216
+ },
217
+ } as DragChangeEvent);
218
+ }
219
+
220
+ if (isPresenting.current) {
221
+ onWillPresent?.({
222
+ nativeEvent: {
223
+ index: toIndex,
224
+ position: animatedPosition.value,
225
+ detent: detents[toIndex] ?? 0,
226
+ },
227
+ } as WillPresentEvent);
228
+
229
+ // Focus events fire together with present events
230
+ onWillFocus?.({ nativeEvent: null } as WillFocusEvent);
231
+ }
232
+
233
+ // Detect if sheet is being restored (will focus)
234
+ if (isMinimized.current && toIndex >= 0) {
235
+ onWillFocus?.({ nativeEvent: null } as WillFocusEvent);
236
+ }
237
+
238
+ if (toIndex === -1 && !isPresenting.current) {
239
+ // Will be handled as blur if the sheet doesn't actually dismiss
240
+ isMinimized.current = true;
241
+ onWillBlur?.({ nativeEvent: null } as WillBlurEvent);
242
+
243
+ if (isDismissing.current) {
244
+ onWillDismiss?.({ nativeEvent: null } as WillDismissEvent);
245
+ }
246
+ }
247
+ },
248
+ [detents, animatedPosition]
249
+ );
250
+
251
+ const backdropComponent = useCallback(
252
+ (backdropProps: BottomSheetBackdropProps) => {
253
+ if (!dimmed) {
254
+ return null;
255
+ }
256
+ return (
257
+ <BottomSheetBackdrop
258
+ {...backdropProps}
259
+ opacity={0.5}
260
+ appearsOnIndex={dimmedDetentIndex}
261
+ disappearsOnIndex={dimmedDetentIndex - 1}
262
+ pressBehavior={dismissible ? 'close' : 'none'}
263
+ />
264
+ );
265
+ },
266
+ [dimmed, dimmedDetentIndex, dismissible]
267
+ );
268
+
269
+ const handleComponent = useCallback(
270
+ (handleProps: BottomSheetHandleProps) => {
271
+ if (!grabber) {
272
+ return null;
273
+ }
274
+ return (
275
+ <BottomSheetHandle
276
+ {...handleProps}
277
+ style={[
278
+ styles.handle,
279
+ grabberOptions?.topMargin !== undefined && { paddingTop: grabberOptions.topMargin },
280
+ ]}
281
+ indicatorStyle={[
282
+ styles.handleIndicator,
283
+ grabberOptions?.width !== undefined && { width: grabberOptions.width },
284
+ grabberOptions?.height !== undefined && { height: grabberOptions.height },
285
+ grabberOptions?.cornerRadius !== undefined && {
286
+ borderRadius: grabberOptions.cornerRadius,
287
+ },
288
+ { backgroundColor: grabberOptions?.color ?? DEFAULT_GRABBER_COLOR },
289
+ ]}
290
+ />
291
+ );
292
+ },
293
+ [grabber, grabberOptions]
294
+ );
295
+
296
+ const footerComponent = useMemo(
297
+ () =>
298
+ footer
299
+ ? (footerProps: BottomSheetFooterProps) => (
300
+ <BottomSheetFooter {...footerProps}>{renderSlot(footer)}</BottomSheetFooter>
301
+ )
302
+ : undefined,
303
+ [footer]
304
+ );
305
+
306
+ // For scrollable, we render the child directly
307
+ const ContainerComponent = scrollable ? Fragment : BottomSheetView;
308
+
309
+ const sheetMethodsRef = useRef<TrueSheetRef>({
310
+ present: async (index = 0) => {
311
+ setSnapIndex(index);
312
+ isPresenting.current = true;
313
+ modalRef.current?.present();
314
+ },
315
+ dismiss: async () => {
316
+ isDismissing.current = true;
317
+ modalRef.current?.dismiss();
318
+ },
319
+ resize: async (index: number) => {
320
+ modalRef.current?.snapToIndex(index);
321
+ },
322
+ });
323
+
324
+ useImperativeHandle(ref, () => sheetMethodsRef.current);
325
+
326
+ // Register with context provider
327
+ useEffect(() => {
328
+ if (name) {
329
+ bottomSheetContext?.register(name, sheetMethodsRef);
330
+ }
331
+ return () => {
332
+ if (name) {
333
+ bottomSheetContext?.unregister(name);
334
+ }
335
+ };
336
+ }, [name]);
337
+
338
+ // Auto-present on mount if initialDetentIndex is set
339
+ useEffect(() => {
340
+ if (initialDetentIndexRef.current >= 0) {
341
+ sheetMethodsRef.current.present(initialDetentIndexRef.current);
342
+ }
343
+ }, []);
344
+
345
+ // Handle mount event after first render
346
+ useEffect(() => {
347
+ if (!isMounted) {
348
+ setIsMounted(true);
349
+ onMount?.({ nativeEvent: null } as MountEvent);
350
+ }
351
+ }, [isMounted, onMount]);
352
+
353
+ return (
354
+ <BottomSheetModal
355
+ ref={modalRef}
356
+ name={name}
357
+ style={[
358
+ styles.root,
359
+ { backgroundColor, borderTopLeftRadius: cornerRadius, borderTopRightRadius: cornerRadius },
360
+ ]}
361
+ index={snapIndex}
362
+ animateOnMount
363
+ enablePanDownToClose={dismissible}
364
+ enableContentPanningGesture={draggable}
365
+ enableHandlePanningGesture={draggable}
366
+ animatedPosition={animatedPosition}
367
+ animatedIndex={animatedIndex}
368
+ handleComponent={handleComponent}
369
+ onChange={handleChange}
370
+ onAnimate={handleAnimate}
371
+ enableDynamicSizing={hasAutoDetent}
372
+ maxDynamicContentSize={maxHeight}
373
+ snapPoints={snapPoints.length > 0 ? snapPoints : undefined}
374
+ onDismiss={handleDismiss}
375
+ stackBehavior="switch"
376
+ backdropComponent={backdropComponent}
377
+ footerComponent={footerComponent}
378
+ >
379
+ <ContainerComponent>
380
+ <View style={[styles.container, style]}>
381
+ {renderSlot(header)}
382
+ {children}
383
+ </View>
384
+ </ContainerComponent>
385
+ </BottomSheetModal>
386
+ );
387
+ });
388
+
389
+ const styles = StyleSheet.create({
390
+ root: {
391
+ overflow: 'hidden',
392
+ },
393
+ container: {},
394
+ handle: {
395
+ position: 'absolute',
396
+ top: 0,
397
+ left: 0,
398
+ right: 0,
399
+ zIndex: 1,
400
+ paddingVertical: 10,
401
+ pointerEvents: 'none',
402
+ },
403
+ handleIndicator: {
404
+ width: 36,
405
+ height: 5,
406
+ },
407
+ });
@@ -0,0 +1,29 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ import { TrueSheet } from './TrueSheet';
4
+ import type { TrueSheetContextMethods } from './TrueSheet.types';
5
+
6
+ export interface TrueSheetProviderProps {
7
+ children: ReactNode;
8
+ }
9
+
10
+ /**
11
+ * Provider for TrueSheet on native platforms.
12
+ * This is a pass-through component - no context is needed on native
13
+ * since TrueSheet uses static instance methods internally.
14
+ */
15
+ export function TrueSheetProvider({ children }: TrueSheetProviderProps) {
16
+ return children;
17
+ }
18
+
19
+ /**
20
+ * Hook to control TrueSheet instances by name.
21
+ * On native, this maps directly to TrueSheet static methods.
22
+ */
23
+ export function useTrueSheet(): TrueSheetContextMethods {
24
+ return {
25
+ present: TrueSheet.present,
26
+ dismiss: TrueSheet.dismiss,
27
+ resize: TrueSheet.resize,
28
+ };
29
+ }
@@ -0,0 +1,81 @@
1
+ import { createContext, useContext, useRef, type ReactNode, type RefObject } from 'react';
2
+ import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
3
+ import type { TrueSheetContextMethods, TrueSheetRef } from './TrueSheet.types';
4
+
5
+ interface BottomSheetContextValue extends TrueSheetContextMethods {
6
+ register: (name: string, methods: RefObject<TrueSheetRef>) => void;
7
+ unregister: (name: string) => void;
8
+ }
9
+
10
+ export const BottomSheetContext = createContext<BottomSheetContextValue | null>(null);
11
+
12
+ export interface TrueSheetProviderProps {
13
+ children: ReactNode;
14
+ }
15
+
16
+ /**
17
+ * Provider for TrueSheet on web.
18
+ * Required to wrap your app for sheet management via useTrueSheet hook.
19
+ */
20
+ export function TrueSheetProvider({ children }: TrueSheetProviderProps) {
21
+ const sheetsRef = useRef<Map<string, RefObject<TrueSheetRef>>>(new Map());
22
+
23
+ const register = (name: string, methods: RefObject<TrueSheetRef>) => {
24
+ sheetsRef.current.set(name, methods);
25
+ };
26
+
27
+ const unregister = (name: string) => {
28
+ sheetsRef.current.delete(name);
29
+ };
30
+
31
+ const present = async (name: string, index: number = 0) => {
32
+ const sheet = sheetsRef.current.get(name);
33
+ if (!sheet?.current) {
34
+ console.warn(`TrueSheet: Could not find sheet with name "${name}"`);
35
+ return;
36
+ }
37
+ return sheet.current.present(index);
38
+ };
39
+
40
+ const dismiss = async (name: string) => {
41
+ const sheet = sheetsRef.current.get(name);
42
+ if (!sheet?.current) {
43
+ console.warn(`TrueSheet: Could not find sheet with name "${name}"`);
44
+ return;
45
+ }
46
+ return sheet.current.dismiss();
47
+ };
48
+
49
+ const resize = async (name: string, index: number) => {
50
+ const sheet = sheetsRef.current.get(name);
51
+ if (!sheet?.current) {
52
+ console.warn(`TrueSheet: Could not find sheet with name "${name}"`);
53
+ return;
54
+ }
55
+ return sheet.current.resize(index);
56
+ };
57
+
58
+ return (
59
+ <BottomSheetContext.Provider value={{ register, unregister, present, dismiss, resize }}>
60
+ <BottomSheetModalProvider>{children}</BottomSheetModalProvider>
61
+ </BottomSheetContext.Provider>
62
+ );
63
+ }
64
+
65
+ /**
66
+ * Hook to control TrueSheet instances by name.
67
+ * On web, this uses the TrueSheetContext from TrueSheetProvider.
68
+ */
69
+ export function useTrueSheet(): TrueSheetContextMethods {
70
+ const context = useContext(BottomSheetContext);
71
+
72
+ if (!context) {
73
+ throw new Error('useTrueSheet must be used within a TrueSheetProvider');
74
+ }
75
+
76
+ return {
77
+ present: context.present,
78
+ dismiss: context.dismiss,
79
+ resize: context.resize,
80
+ };
81
+ }
package/src/index.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from './TrueSheet';
2
2
  export * from './TrueSheet.types';
3
+ export * from './TrueSheetProvider';
@@ -0,0 +1,78 @@
1
+ import { forwardRef, useEffect } from 'react';
2
+ import { useWindowDimensions } from 'react-native';
3
+
4
+ import { TrueSheet } from '../TrueSheet.web';
5
+ import type { TrueSheetProps, TrueSheetRef, PositionChangeEvent } from '../TrueSheet.types';
6
+ import { useReanimatedTrueSheet } from './ReanimatedTrueSheetProvider';
7
+
8
+ interface ReanimatedTrueSheetProps extends TrueSheetProps {
9
+ /**
10
+ * Callback for position changes.
11
+ * On web, this is called with the position data from @gorhom/bottom-sheet.
12
+ *
13
+ * @see {@link TrueSheetProps.onPositionChange}
14
+ */
15
+ onPositionChange?: TrueSheetProps['onPositionChange'];
16
+ }
17
+
18
+ /**
19
+ * Reanimated-enabled version of TrueSheet for web that automatically syncs
20
+ * position with the provider's shared value.
21
+ * Must be used within a ReanimatedTrueSheetProvider.
22
+ *
23
+ * @example
24
+ * ```tsx
25
+ * import { ReanimatedTrueSheet, ReanimatedTrueSheetProvider } from '@lodev09/react-native-true-sheet/reanimated'
26
+ *
27
+ * function MyScreen() {
28
+ * const sheetRef = useRef<TrueSheetRef>(null)
29
+ *
30
+ * return (
31
+ * <ReanimatedTrueSheetProvider>
32
+ * <View>
33
+ * <ReanimatedTrueSheet
34
+ * ref={sheetRef}
35
+ * detents={[0.25, 0.5, 1]}
36
+ * initialDetentIndex={1}
37
+ * >
38
+ * <Text>Sheet Content</Text>
39
+ * </ReanimatedTrueSheet>
40
+ * </View>
41
+ * </ReanimatedTrueSheetProvider>
42
+ * )
43
+ * }
44
+ * ```
45
+ */
46
+ export const ReanimatedTrueSheet = forwardRef<TrueSheetRef, ReanimatedTrueSheetProps>(
47
+ (props, ref) => {
48
+ const { onPositionChange, detents = [0.5, 1], ...rest } = props;
49
+ const { height: windowHeight } = useWindowDimensions();
50
+
51
+ const { animatedPosition, animatedIndex, animatedDetent } = useReanimatedTrueSheet();
52
+
53
+ // Reset animated values when component unmounts
54
+ useEffect(() => {
55
+ return () => {
56
+ animatedPosition.value = windowHeight;
57
+ animatedIndex.value = -1;
58
+ animatedDetent.value = 0;
59
+ };
60
+ }, [windowHeight]);
61
+
62
+ const handlePositionChange = (event: PositionChangeEvent) => {
63
+ const { position, index, detent } = event.nativeEvent;
64
+
65
+ // Sync with provider's shared values
66
+ animatedPosition.value = position;
67
+ animatedIndex.value = index;
68
+ animatedDetent.value = detent;
69
+
70
+ // Call user's callback
71
+ onPositionChange?.(event);
72
+ };
73
+
74
+ return (
75
+ <TrueSheet ref={ref} detents={detents} onPositionChange={handlePositionChange} {...rest} />
76
+ );
77
+ }
78
+ );
@@ -0,0 +1,32 @@
1
+ import { useCallback } from 'react';
2
+ import type { PositionChangeEvent, PositionChangeEventPayload } from '../TrueSheet.types';
3
+
4
+ type PositionChangeHandler = (
5
+ payload: PositionChangeEventPayload,
6
+ context: Record<string, unknown>
7
+ ) => void;
8
+
9
+ /**
10
+ * Web implementation of useReanimatedPositionChangeHandler.
11
+ *
12
+ * On web, this returns a simple callback wrapper since @gorhom/bottom-sheet
13
+ * already provides animated position values. The worklet directive is ignored
14
+ * on web as there's no native UI thread.
15
+ *
16
+ * @param handler - The position change handler function
17
+ * @param _dependencies - Unused on web, kept for API compatibility
18
+ * @returns An event handler compatible with onPositionChange prop
19
+ */
20
+ export const useReanimatedPositionChangeHandler = (
21
+ handler: PositionChangeHandler,
22
+ _dependencies: unknown[] = []
23
+ ) => {
24
+ const context: Record<string, unknown> = {};
25
+
26
+ return useCallback(
27
+ (event: PositionChangeEvent) => {
28
+ handler(event.nativeEvent, context);
29
+ },
30
+ [handler]
31
+ );
32
+ };