diff --git a/README.md b/README.md index b4845cd2..b4757cb4 100644 --- a/README.md +++ b/README.md @@ -73,10 +73,21 @@ yarn add react-native-sortables This library is built with: - [react-native-reanimated](https://docs.swmansion.com/react-native-reanimated/) (version 3.x, 4.x) -- [react-native-gesture-handler](https://docs.swmansion.com/react-native-gesture-handler/) (version 2.x) +- [react-native-gesture-handler](https://docs.swmansion.com/react-native-gesture-handler/) (version 2.x, 3.x) Make sure to follow their installation instructions for your project. +> [!NOTE] +> On iOS + New Architecture, gesture-handler **2.x** has a known limitation: a +> sortable stops recognizing drags after its screen is detached and re-attached +> (for example bottom tabs with `detachInactiveScreens={false}`), which can make +> a dragged item get "stuck" - see +> [#349](https://github.com/MatiPl01/react-native-sortables/issues/349). This is +> not fixable on the v2 API and is resolved by upgrading to +> **gesture-handler 3** (which the library uses automatically when installed; v3 +> requires the New Architecture). On the Old Architecture (Paper) the issue does +> not occur, so gesture-handler 2.x remains the right choice there. + ## Quick Start ```tsx diff --git a/example/app/jest.config.ts b/example/app/jest.config.ts index 8c7ab586..ada82f1a 100644 --- a/example/app/jest.config.ts +++ b/example/app/jest.config.ts @@ -9,9 +9,15 @@ const config: JestConfigWithTsJest = { }, moduleDirectories: ['node_modules', '../../node_modules', ''], moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], - moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths ?? {}, { - prefix: '/' - }), + moduleNameMapper: { + ...pathsToModuleNameMapper(compilerOptions.paths ?? {}, { + prefix: '/' + }), + // react-native-sortables is built against gesture-handler v3, but the app's + // jest mock targets v2; pin every import to the single mocked instance. + '^react-native-gesture-handler$': + '/../../node_modules/react-native-gesture-handler' + }, preset: '@react-native/jest-preset', resolver: 'react-native-worklets/jest/resolver.js', setupFilesAfterEnv: ['/jest.setup.js'], diff --git a/packages/react-native-sortables/package.json b/packages/react-native-sortables/package.json index 56bf60f4..241e6ef9 100644 --- a/packages/react-native-sortables/package.json +++ b/packages/react-native-sortables/package.json @@ -25,7 +25,7 @@ "react": "19.2.3", "react-native": "0.85.3", "react-native-builder-bob": "0.40.13", - "react-native-gesture-handler": "2.31.1", + "react-native-gesture-handler": "3.0.2", "react-native-reanimated": "4.5.0", "react-native-worklets": "0.10.0", "semantic-release": "^24.0.0", diff --git a/packages/react-native-sortables/src/components/shared/CustomHandle.tsx b/packages/react-native-sortables/src/components/shared/CustomHandle.tsx index 9ff327ad..5c9a27f0 100644 --- a/packages/react-native-sortables/src/components/shared/CustomHandle.tsx +++ b/packages/react-native-sortables/src/components/shared/CustomHandle.tsx @@ -3,6 +3,7 @@ import type { StyleProp, ViewStyle } from 'react-native'; import { View } from 'react-native'; import { runOnUI, useAnimatedRef } from 'react-native-reanimated'; +import { useEnabledGesture } from '../../integrations/gesture-handler'; import { useCustomHandleContext, useIsInPortalOutlet, @@ -61,6 +62,7 @@ function CustomHandleComponent({ const { registerHandle, updateActiveHandleMeasurements } = customHandleContext; const dragEnabled = mode === 'draggable'; + const handleGesture = useEnabledGesture(gesture, dragEnabled); useEffect(() => { return registerHandle(itemKey, handleRef, mode === 'fixed-order'); @@ -74,7 +76,7 @@ function CustomHandleComponent({ }, [itemKey, isActive, updateActiveHandleMeasurements]); return ( - + & { commonValuesContext: CommonValuesContextType; - gesture: GestureType; + gesture: SortableGesture; onTeleport: (isTeleported: boolean) => void; }; diff --git a/packages/react-native-sortables/src/components/shared/SortableGestureDetector.tsx b/packages/react-native-sortables/src/components/shared/SortableGestureDetector.tsx index 99c125fd..21ff52fb 100644 --- a/packages/react-native-sortables/src/components/shared/SortableGestureDetector.tsx +++ b/packages/react-native-sortables/src/components/shared/SortableGestureDetector.tsx @@ -9,6 +9,12 @@ export type SortableGestureDetectorProps = PropsWithChildren<{ gesture: ComposedGesture | GestureType; }>; +// Cast `GestureDetector` to its legacy props so it accepts the cross-major +// `SortableGesture` union (its exported types otherwise reject it). +const Detector = GestureDetector as ( + props: SortableGestureDetectorProps +) => ReturnType; + /** * Wrapper over gesture handler's `GestureDetector` used by all draggable item * parts. On native it is a passthrough; the web counterpart (`.web`) layers on @@ -18,5 +24,5 @@ export default function SortableGestureDetector({ children, gesture }: SortableGestureDetectorProps) { - return {children}; + return {children}; } diff --git a/packages/react-native-sortables/src/components/shared/SortableGestureDetector.web.tsx b/packages/react-native-sortables/src/components/shared/SortableGestureDetector.web.tsx index cecbcadc..cc002349 100644 --- a/packages/react-native-sortables/src/components/shared/SortableGestureDetector.web.tsx +++ b/packages/react-native-sortables/src/components/shared/SortableGestureDetector.web.tsx @@ -29,6 +29,15 @@ export type SortableGestureDetectorProps = PropsWithChildren<{ gesture: ComposedGesture | GestureType; }>; +// Cast `GestureDetector` to its legacy props (plus web layout props) so it +// accepts the cross-major `SortableGesture` union. +const Detector = GestureDetector as ( + props: SortableGestureDetectorProps & { + touchAction?: 'pan-x' | 'pan-y'; + userSelect?: 'none'; + } +) => ReturnType; + /** * Web `GestureDetector`: relaxes `touch-action` to the scroll axis (so items * don't block scrolling the ScrollView) and blocks native scroll while dragging. @@ -67,11 +76,8 @@ export default function SortableGestureDetector({ useEffect(() => () => setBlocking(false), [setBlocking]); return ( - + {children} - + ); } diff --git a/packages/react-native-sortables/src/components/shared/SortableTouchable.tsx b/packages/react-native-sortables/src/components/shared/SortableTouchable.tsx index b774195e..012eab2f 100644 --- a/packages/react-native-sortables/src/components/shared/SortableTouchable.tsx +++ b/packages/react-native-sortables/src/components/shared/SortableTouchable.tsx @@ -1,9 +1,8 @@ -import { type PropsWithChildren, useMemo } from 'react'; +import { type PropsWithChildren } from 'react'; import type { ViewProps } from 'react-native'; import { View } from 'react-native'; -import type { GestureType } from 'react-native-gesture-handler'; -import { Gesture } from 'react-native-gesture-handler'; +import { useTouchableGesture } from '../../integrations/gesture-handler'; import { useItemContext } from '../../providers'; import SortableGestureDetector from './SortableGestureDetector'; @@ -32,65 +31,16 @@ export default function SortableTouchable({ }: SortableTouchableProps) { const { gesture: externalGesture } = useItemContext(); - const gesture = useMemo(() => { - const decorate = (decoratedGesture: T): T => { - decoratedGesture - .simultaneousWithExternalGesture(externalGesture) - .runOnJS(true); - if ('maxDistance' in decoratedGesture) { - ( - decoratedGesture as { maxDistance: (distance: number) => GestureType } - ).maxDistance(failDistance); - } - return decoratedGesture; - }; - - const gestures = []; - - if (onTap) { - gestures.push(decorate(Gesture.Tap()).onStart(onTap)); - } - - if (onDoubleTap) { - gestures.push( - decorate(Gesture.Tap()).numberOfTaps(2).onStart(onDoubleTap) - ); - } - - if (onLongPress) { - gestures.push(decorate(Gesture.LongPress()).onStart(onLongPress)); - } - - if (onTouchesDown || onTouchesUp) { - // Reuse already added gesture if possible or create a manual gesture - // if there is no other gesture yet - if (!gestures.length) { - gestures.push(decorate(Gesture.Manual())); - } - - const lastGesture = gestures[gestures.length - 1]!; - - if (onTouchesDown) { - lastGesture.onTouchesDown(onTouchesDown); - } - if (onTouchesUp) { - lastGesture.onTouchesUp(onTouchesUp); - } - } - - return gestureMode === 'exclusive' - ? Gesture.Exclusive(...gestures) - : Gesture.Simultaneous(...gestures); - }, [ + const gesture = useTouchableGesture({ + externalGesture, failDistance, - onTap, + gestureMode, onDoubleTap, onLongPress, + onTap, onTouchesDown, - onTouchesUp, - externalGesture, - gestureMode - ]); + onTouchesUp + }); return ( diff --git a/packages/react-native-sortables/src/integrations/gesture-handler/adapters/types.ts b/packages/react-native-sortables/src/integrations/gesture-handler/adapters/types.ts new file mode 100644 index 00000000..806655fd --- /dev/null +++ b/packages/react-native-sortables/src/integrations/gesture-handler/adapters/types.ts @@ -0,0 +1,22 @@ +import type { + ManualGestureCallbacks, + SortableGesture, + TouchableGestureConfig +} from '../types'; + +/** + * Unified gesture API used across the library. Each gesture-handler major + * version provides its own implementation (v2 imperative builder, v3 hook API), + * selected once at module load based on which version is installed. + */ +export type GestureHandlerAdapter = { + useDragGesture: ( + callbacks: ManualGestureCallbacks, + deps: ReadonlyArray + ) => SortableGesture; + useEnabledGesture: ( + gesture: SortableGesture, + enabled: boolean + ) => SortableGesture; + useTouchableGesture: (config: TouchableGestureConfig) => SortableGesture; +}; diff --git a/packages/react-native-sortables/src/integrations/gesture-handler/adapters/v2.ts b/packages/react-native-sortables/src/integrations/gesture-handler/adapters/v2.ts new file mode 100644 index 00000000..0fb59982 --- /dev/null +++ b/packages/react-native-sortables/src/integrations/gesture-handler/adapters/v2.ts @@ -0,0 +1,108 @@ +import { useMemo } from 'react'; +import type { GestureType } from 'react-native-gesture-handler'; +import { Gesture } from 'react-native-gesture-handler'; + +import { asSortableGesture } from '../types'; +import type { GestureHandlerAdapter } from './types'; + +/** + * gesture-handler v2 imperative builder, used when gesture-handler < 3 is + * installed (e.g. the Old Architecture / Paper). On iOS + New Architecture this + * path still has the upstream issue #349 limitation, which only the v3 hook API + * fixes (see `./v3`). + */ + +const useDragGesture: GestureHandlerAdapter['useDragGesture'] = ( + callbacks, + deps +) => + asSortableGesture( + useMemo( + () => + Gesture.Manual() + .onTouchesDown(callbacks.onTouchesDown) + .onTouchesMove(callbacks.onTouchesMove) + .onTouchesCancelled(callbacks.onTouchesCancelled) + .onTouchesUp(callbacks.onTouchesUp), + // The dependency list is owned by the caller (useItemPanGesture). + // eslint-disable-next-line react-hooks/exhaustive-deps + deps + ) + ); + +const useEnabledGesture: GestureHandlerAdapter['useEnabledGesture'] = ( + gesture, + enabled +) => asSortableGesture((gesture as GestureType).enabled(enabled)); + +const useTouchableGesture: GestureHandlerAdapter['useTouchableGesture'] = ({ + externalGesture, + failDistance, + gestureMode, + onDoubleTap, + onLongPress, + onTap, + onTouchesDown, + onTouchesUp +}) => + useMemo(() => { + const decorate = (gesture: T): T => { + gesture + .simultaneousWithExternalGesture(externalGesture as GestureType) + .runOnJS(true); + if ('maxDistance' in gesture) { + ( + gesture as { maxDistance: (distance: number) => GestureType } + ).maxDistance(failDistance); + } + return gesture; + }; + + const gestures: Array = []; + + if (onTap) { + gestures.push(decorate(Gesture.Tap()).onStart(onTap)); + } + if (onDoubleTap) { + gestures.push( + decorate(Gesture.Tap()).numberOfTaps(2).onStart(onDoubleTap) + ); + } + if (onLongPress) { + gestures.push(decorate(Gesture.LongPress()).onStart(onLongPress)); + } + + if (onTouchesDown || onTouchesUp) { + const target = gestures.at(-1) ?? decorate(Gesture.Manual()); + if (!gestures.length) { + gestures.push(target); + } + if (onTouchesDown) { + target.onTouchesDown(onTouchesDown); + } + if (onTouchesUp) { + target.onTouchesUp(onTouchesUp); + } + } + + return asSortableGesture( + gestureMode === 'exclusive' + ? Gesture.Exclusive(...gestures) + : Gesture.Simultaneous(...gestures) + ); + }, [ + failDistance, + onTap, + onDoubleTap, + onLongPress, + onTouchesDown, + onTouchesUp, + externalGesture, + gestureMode + ]); + +export const adapter: GestureHandlerAdapter = { + useDragGesture, + useEnabledGesture, + useTouchableGesture +}; diff --git a/packages/react-native-sortables/src/integrations/gesture-handler/adapters/v3.ts b/packages/react-native-sortables/src/integrations/gesture-handler/adapters/v3.ts new file mode 100644 index 00000000..2ea84e99 --- /dev/null +++ b/packages/react-native-sortables/src/integrations/gesture-handler/adapters/v3.ts @@ -0,0 +1,169 @@ +import type { SingleGesture } from 'react-native-gesture-handler'; +import * as GestureHandler from 'react-native-gesture-handler'; +import type { SharedValue } from 'react-native-reanimated'; + +import { useMutableValue } from '../../reanimated'; +import type { ManualGestureControl } from '../types'; +import { asSortableGesture } from '../types'; +import type { GestureHandlerAdapter } from './types'; + +/** + * gesture-handler v3 hook API, used when gesture-handler >= 3 is installed. This + * is what fixes issue #349: v3 hook gestures keep receiving touches after a + * screen is detached and re-attached, so a drag no longer gets stuck. + * + * The hooks are resolved through a namespace import so bundlers don't fail on + * these (v3-only) names when an older gesture-handler is installed - this + * adapter is only ever selected when they exist (see `../index`). + */ +const { + GestureStateManager, + useExclusiveGestures, + useLongPressGesture, + useManualGesture, + useSimultaneousGestures, + useTapGesture +} = GestureHandler; + +function createControl( + handlerTag: number, + pendingActivation: SharedValue +): ManualGestureControl { + 'worklet'; + return { + // `GestureStateManager.activate` throws when called outside a gesture event, + // but the library activates from a delayed timeout - so flag it here and run + // the real activation in the next in-event `onTouchesMove`. + activate: () => { + 'worklet'; + pendingActivation.value = true; + }, + end: () => { + 'worklet'; + GestureStateManager.deactivate(handlerTag); + }, + fail: () => { + 'worklet'; + pendingActivation.value = false; + GestureStateManager.fail(handlerTag); + } + }; +} + +// Unlike the v2 builder, v3 hooks re-apply their config (and worklet callbacks) +// on every render, so the caller's `deps` list is not needed here. +const useDragGesture: GestureHandlerAdapter['useDragGesture'] = callbacks => { + const pendingActivation = useMutableValue(false); + + return asSortableGesture( + useManualGesture({ + onTouchesCancel: event => { + 'worklet'; + callbacks.onTouchesCancelled( + event, + createControl(event.handlerTag, pendingActivation) + ); + }, + onTouchesDown: event => { + 'worklet'; + pendingActivation.value = false; + callbacks.onTouchesDown( + event, + createControl(event.handlerTag, pendingActivation) + ); + }, + onTouchesMove: event => { + 'worklet'; + if (pendingActivation.value) { + pendingActivation.value = false; + GestureStateManager.activate(event.handlerTag); + } + callbacks.onTouchesMove( + event, + createControl(event.handlerTag, pendingActivation) + ); + }, + onTouchesUp: event => { + 'worklet'; + callbacks.onTouchesUp( + event, + createControl(event.handlerTag, pendingActivation) + ); + } + }) + ); +}; + +const useEnabledGesture: GestureHandlerAdapter['useEnabledGesture'] = ( + gesture, + enabled +) => { + const current = gesture as { config?: object }; + return asSortableGesture({ + ...current, + config: { ...current.config, enabled } + }); +}; + +const useTouchableGesture: GestureHandlerAdapter['useTouchableGesture'] = ({ + externalGesture, + failDistance, + gestureMode, + onDoubleTap, + onLongPress, + onTap, + onTouchesDown, + onTouchesUp +}) => { + const simultaneousWith = externalGesture as object as SingleGesture; + + // Hooks run unconditionally, so every gesture is always created; the ones + // without a handler stay disabled. + const tap = useTapGesture({ + enabled: !!onTap, + maxDistance: failDistance, + onActivate: onTap, + runOnJS: true, + simultaneousWith + }); + const doubleTap = useTapGesture({ + enabled: !!onDoubleTap, + maxDistance: failDistance, + numberOfTaps: 2, + onActivate: onDoubleTap, + runOnJS: true, + simultaneousWith + }); + const longPress = useLongPressGesture({ + enabled: !!onLongPress, + maxDistance: failDistance, + onActivate: onLongPress, + runOnJS: true, + simultaneousWith + }); + const manual = useManualGesture({ + enabled: !!(onTouchesDown ?? onTouchesUp), + onTouchesDown, + onTouchesUp, + runOnJS: true, + simultaneousWith + }); + + const exclusive = useExclusiveGestures(tap, doubleTap, longPress, manual); + const simultaneous = useSimultaneousGestures( + tap, + doubleTap, + longPress, + manual + ); + + return asSortableGesture( + gestureMode === 'exclusive' ? exclusive : simultaneous + ); +}; + +export const adapter: GestureHandlerAdapter = { + useDragGesture, + useEnabledGesture, + useTouchableGesture +}; diff --git a/packages/react-native-sortables/src/integrations/gesture-handler/index.ts b/packages/react-native-sortables/src/integrations/gesture-handler/index.ts new file mode 100644 index 00000000..f062b6b7 --- /dev/null +++ b/packages/react-native-sortables/src/integrations/gesture-handler/index.ts @@ -0,0 +1,25 @@ +import * as GestureHandler from 'react-native-gesture-handler'; + +import { adapter as v2Adapter } from './adapters/v2'; +import { adapter as v3Adapter } from './adapters/v3'; + +// The v3 hook API only exists in gesture-handler >= 3. Pick the adapter once at +// module load (the installed version is fixed at runtime) so the chosen hooks are +// called in a stable order across renders. +const hasHookApi = + typeof (GestureHandler as { useManualGesture?: unknown }).useManualGesture === + 'function'; + +const adapter = hasHookApi ? v3Adapter : v2Adapter; + +export const useDragGesture = adapter.useDragGesture; +export const useEnabledGesture = adapter.useEnabledGesture; +export const useTouchableGesture = adapter.useTouchableGesture; + +export type { + GestureTouchEvent, + ManualGestureControl, + SortableGesture, + TouchableGestureConfig, + TouchData +} from './types'; diff --git a/packages/react-native-sortables/src/integrations/gesture-handler/types.ts b/packages/react-native-sortables/src/integrations/gesture-handler/types.ts new file mode 100644 index 00000000..a7f9aa3d --- /dev/null +++ b/packages/react-native-sortables/src/integrations/gesture-handler/types.ts @@ -0,0 +1,58 @@ +import type { + ComposedGesture, + GestureTouchEvent, + GestureType, + TouchData +} from 'react-native-gesture-handler'; + +export type { GestureTouchEvent, TouchData }; + +/** + * A gesture handed to ``. Typed with the only gesture names + * exported by both gesture-handler majors, so published types resolve for + * consumers on either version. + */ +export type SortableGesture = ComposedGesture | GestureType; + +export const asSortableGesture = (gesture: object): SortableGesture => + gesture as SortableGesture; + +/** + * Imperative control over a manual gesture's recognition state - the v2 gesture + * `manager`, or the v3 `GestureStateManager` bound to the handler tag. + */ +export type ManualGestureControl = { + activate: () => void; + end: () => void; + fail: () => void; +}; + +export type ManualGestureCallbacks = { + onTouchesCancelled: ( + event: GestureTouchEvent, + control: ManualGestureControl + ) => void; + onTouchesDown: ( + event: GestureTouchEvent, + control: ManualGestureControl + ) => void; + onTouchesMove: ( + event: GestureTouchEvent, + control: ManualGestureControl + ) => void; + onTouchesUp: ( + event: GestureTouchEvent, + control: ManualGestureControl + ) => void; +}; + +export type TouchableGestureConfig = { + externalGesture: SortableGesture; + failDistance: number; + gestureMode: 'exclusive' | 'simultaneous'; + onDoubleTap?: () => void; + onLongPress?: () => void; + onTap?: () => void; + onTouchesDown?: () => void; + onTouchesUp?: () => void; +}; diff --git a/packages/react-native-sortables/src/providers/shared/hooks/useItemPanGesture.ts b/packages/react-native-sortables/src/providers/shared/hooks/useItemPanGesture.ts index b1f67b7f..7aa4c06f 100644 --- a/packages/react-native-sortables/src/providers/shared/hooks/useItemPanGesture.ts +++ b/packages/react-native-sortables/src/providers/shared/hooks/useItemPanGesture.ts @@ -1,7 +1,6 @@ -import { useMemo } from 'react'; -import { Gesture } from 'react-native-gesture-handler'; import type { SharedValue } from 'react-native-reanimated'; +import { useDragGesture } from '../../../integrations/gesture-handler'; import { useDragContext } from '../DragProvider'; export default function useItemPanGesture( @@ -11,29 +10,33 @@ export default function useItemPanGesture( const { handleDragEnd, handleTouchesMove, handleTouchStart } = useDragContext(); - return useMemo( - () => - Gesture.Manual() - .onTouchesDown((e, manager) => { - handleTouchStart( - e, - key, - activationAnimationProgress, - manager.activate, - manager.fail - ); - }) - .onTouchesMove((e, manager) => { - handleTouchesMove(e, manager.fail); - }) - .onTouchesCancelled((_, manager) => { - handleDragEnd(key, activationAnimationProgress); - manager.fail(); - }) - .onTouchesUp((_, manager) => { - handleDragEnd(key, activationAnimationProgress); - manager.end(); - }), + return useDragGesture( + { + onTouchesCancelled: (_event, control) => { + 'worklet'; + handleDragEnd(key, activationAnimationProgress); + control.fail(); + }, + onTouchesDown: (event, control) => { + 'worklet'; + handleTouchStart( + event, + key, + activationAnimationProgress, + control.activate, + control.fail + ); + }, + onTouchesMove: (event, control) => { + 'worklet'; + handleTouchesMove(event, control.fail); + }, + onTouchesUp: (_event, control) => { + 'worklet'; + handleDragEnd(key, activationAnimationProgress); + control.end(); + } + }, [ handleDragEnd, handleTouchStart, diff --git a/packages/react-native-sortables/src/types/providers/shared.ts b/packages/react-native-sortables/src/types/providers/shared.ts index b80185e6..5c4bcc9a 100644 --- a/packages/react-native-sortables/src/types/providers/shared.ts +++ b/packages/react-native-sortables/src/types/providers/shared.ts @@ -1,9 +1,6 @@ import type { ReactNode } from 'react'; import type { ScrollView, View } from 'react-native'; -import type { - GestureTouchEvent, - GestureType -} from 'react-native-gesture-handler'; +import type { GestureTouchEvent } from 'react-native-gesture-handler'; import type { AnimatedRef, MeasuredDimensions, @@ -11,6 +8,7 @@ import type { } from 'react-native-reanimated'; import type { DeepReadonly, Maybe, Simplify } from '../../helperTypes'; +import type { SortableGesture } from '../../integrations/gesture-handler'; import type { AnimatedValues } from '../../integrations/reanimated'; import type { DebugCrossUpdater, @@ -159,7 +157,7 @@ export type ItemContextType = Simplify< activationAnimationProgress: SharedValue; } > & { - gesture: GestureType; + gesture: SortableGesture; } >; diff --git a/yarn.lock b/yarn.lock index 6468fb60..d4d5baf4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14919,6 +14919,19 @@ __metadata: languageName: node linkType: hard +"react-native-gesture-handler@npm:3.0.2": + version: 3.0.2 + resolution: "react-native-gesture-handler@npm:3.0.2" + dependencies: + "@types/react-test-renderer": "npm:^19.1.0" + invariant: "npm:^2.2.4" + peerDependencies: + react: "*" + react-native: "*" + checksum: 10c0/06d2b1a621f73c148c4000692e55d5f2a44d44229cabc79829de0b0b3dbf3bcf4bc00e8635f82bd1062d7eac87f39ca5213eb1041e23a5543947e64773eaddfb + languageName: node + linkType: hard + "react-native-gesture-handler@npm:~2.31.1": version: 2.31.2 resolution: "react-native-gesture-handler@npm:2.31.2" @@ -15126,7 +15139,7 @@ __metadata: react: "npm:19.2.3" react-native: "npm:0.85.3" react-native-builder-bob: "npm:0.40.13" - react-native-gesture-handler: "npm:2.31.1" + react-native-gesture-handler: "npm:3.0.2" react-native-haptic-feedback: "npm:>=2.0.0" react-native-reanimated: "npm:4.5.0" react-native-worklets: "npm:0.10.0"