Skip to content

Merge Gesture Handler 3 working branch to main#3954

Merged
j-piasecki merged 237 commits intomainfrom
next
Feb 5, 2026
Merged

Merge Gesture Handler 3 working branch to main#3954
j-piasecki merged 237 commits intomainfrom
next

Conversation

@j-piasecki
Copy link
Copy Markdown
Member

Description

Merges the next branch, where the Gesture Handler 3 was developed into main.

latekvo and others added 30 commits July 2, 2025 14:36
<!--
Description and motivation for this PR.

Include 'Fixes #<number>' if this is fixing some issue.
-->

Removed legacy `Hammer.JS` web implementation, all it's usages and
references.
Removed `enableLegacyWebImplementation`.
Removed `isNewWebImplementationEnabled`. It's not abundantly clear at
first, but it's an internal function.
…d UI runtime (#3207)

## Description

Changes how `setGestureState` is exposed to the UI runtime. Instead of
the weird conditionally adding Reanimated as a dependency on Android and
the weird cast on iOS it uses `_WORKLET_RUNTIME` const injected by
Reanimated into the JS runtime. This allows Gesture Handler to decorate
the UI runtime without direct dependencies between the libraries.

The new approach relies on two methods being added to the global object:
- `_setGestureStateAsync` on the JS runtime
- `_setGestureStateSync` on the UI runtime

which allows for state manipulation also from the JS thread.

The basic example has been modified to easily test the new
functionality.

> [!CAUTION]
> This works only on the New Architecture (and breaks the old one)

## Test plan

Test the expo example app and the modified basic example app
## Description

Merges declaration and initialization of fields in the event builder
classes

## Test plan

Build android
## Description

Remove actions testing build for the old architecture

## Test plan

See CI
## Description

### Problem

Currently the following configuration:

```jsx
<GestureDetector ... >
  <Text> ... </Text>
</GestureDetector>
```

does not work on `iOS`. This is due to change `react-native` introduced in 0.79 - `hitTest` in `RCTParagraphTextView` now returns `nil` by default ([see here](https://github.com/facebook/react-native/blob/dcbbf275cbc4150820691a4fbc254b198cc92bdd/packages/react-native/React/Fabric/Mounting/ComponentViews/Text/RCTParagraphComponentView.mm#L379)). This results in native `UIGestureRecognizer` not responding to touches.

### Solution

We no longer attach native recognizer to `RCTParagraphTextView`, but to its parent - `RCTParagraphComponentView`. The problem with this approach is that `handleGesture` method uses `reactTag` property, which on `RCTParagraphComponentView` is `nil`. This is why we use `reactTag` from `RCTParagraphTextView` when sending event to `JS` side.

Fixes #3581

## Test plan

<details>
<summary>Tested on the following code:</summary>

```jsx
import React from 'react';
import { StyleSheet, Text } from 'react-native';
import {
  Gesture,
  GestureDetector,
  GestureHandlerRootView,
} from 'react-native-gesture-handler';

export default function EmptyExample() {
  const g = Gesture.Tap().onEnd(() => {
    console.log('Tapped!');
  });

  return (
    <GestureHandlerRootView style={styles.container}>
      <GestureDetector gesture={g}>
        <Text>
          Click me
          <Text> Me too! </Text>
        </Text>
      </GestureDetector>
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
});
```

</details>
…3596)

## Description

Logic behind sending touch events on `android` dispatches events into all gesture handlers registered in the orchestrator. This means that interaction with one `GestureDetector` triggers callbacks on the others. This PR adds check for tracked pointer, so that handlers respond only to those that they are tracking.

Fixes #3543

## Test plan

<details>

<summary>Tested on the following example:</summary>

```jsx
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import {
  Gesture,
  GestureDetector,
  GestureHandlerRootView,
} from 'react-native-gesture-handler';

type BoxProps = {
  label: string;
};

function Box({ label }: BoxProps) {
  const manual = Gesture.Manual()
    .onTouchesDown((e) => {
      console.log('down', label, e.handlerTag);
    })
    .onTouchesUp((e) => {
      console.log('up', label, e.handlerTag);
    })
    .onTouchesCancelled(() => {
      console.log('cancelled', label);
    });

  return (
    <GestureDetector gesture={manual}>
      <View style={styles.box}>
        <Text style={styles.text}>{label}</Text>
      </View>
    </GestureDetector>
  );
}

export default function EmptyExample() {
  return (
    <GestureHandlerRootView style={styles.container}>
      <Box label="1" />
      <Box label="2" />
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    gap: 10,
  },
  box: {
    width: 100,
    height: 100,
    backgroundColor: 'red',
    alignItems: 'center',
    justifyContent: 'center',
  },
  text: {
    color: 'white',
    fontSize: 20,
    fontWeight: 'bold',
  },
});
```

</details>
## Changes

- Replace `forwardRef` with prop `ref`
- Fix possible infinite re-render bug
- Fix invalid dependency lists
- Split `ReanimatedSwipeable` into 3 smaller files: 
  - `index.ts`
  - `ReanimatedSwipeable.tsx`
  - `ReanimatedSwipeableProps.tsx`

No changes to the core logic have been made.

## Test plan

Use available Swipeable examples to test if it works.
This PR adds a Radon IDE banner to the right-hand side part of the
documentation.


![image](https://github.com/user-attachments/assets/e7ee0fe9-e6fa-4f23-882d-07d783297aa3)

The banner rotates a couple of different labels and CTAs. It's only
visible on desktop.

Related to
software-mansion/react-native-reanimated#7631
and
software-mansion/react-native-reanimated#7634
## Description

Some of our users experience crashes that `reversed` is not defined:

```
java.lang.NoSuchMethodError: No virtual method reversed()Ljava/util/List;
```

This PR changes `reversed` occurrences to either `asReversed` (if possible), or `asReversed().toList()`, if array can be modified.

Closes #3594 

## Test plan

Tested on expo-example (mostly _transformations_, _multitap_).
## Description

#3579 introduced changes to `ReanimatedSwipeable`. However, it didn't update path to component in `tsconfig`, thus CI was failing.

## Test plan

Check CI (+ `yarn ts-check`)
## Description

Implements a native `RNGestureHandlerDetector` component based on
`display: contents`:
- The view will match the layout of its child
- The lifecycle of gestures is moved to hooks instead of being managed
by the detector
- Detector should attach and detach native gesture handlers to itself as
it's mounted or unmounted
- Implements sending native events to the detector component
- It's able to work with RN's Animated events

TODO:
- Validate that gestures are correctly attached/detached in all cases
(suspense, display: none, navigation)
- Validate that the gesture lifecycle is correctly managed in StrictMode
- Correctly handle `NativeViewGestureHandler`, which should be attached
to the child (I think)
- Correctly handle the Text component
- Integration with Reanimated

## Test plan

Updated basic example
## Description

Rendering `Pressable` during an active press resulted in `onPress` never
being called.
This PR moves `StateMachine` dependencies, such that it is not reset
when `handlePressIn` or `handlePressOut` change.

Fixes #3593

## Test plan

- Use the repro provided within #3593
- See how `onPress` is called correctly
## Description

see https://gitlab.com/IzzyOnDroid/repo/-/wikis/Reproducible-Builds/RB-Hints-for-Developers#no-funny-build-time-generated-ids for context.

## Test plan

I've successfully used these changes for a little while on my own app.
## Description

The `Pressable` was developed with an incorrect assumption that the
`onLayout` prop is not available on the `NativeButton`.

This PR defines the `onLayout` on `RawButtonProps`, and makes use of
said prop in `Pressable` to remove the need for the
`dimensionsAfterResize` property.

This PR also marks `dimensionsAfterResize` as deprecated.

Fixes #3600

## Test plan

1. Use the provided test code.
2. Notice how the `Pressable`, despite starting out with `0, 0`
dimensions, responds correctly to all press events.
3. Use the `Pressable` examples in our example app to confirm there are
no new issues with the component.

## Test code

<details>

```tsx
import React, { useEffect } from 'react';
import { StyleSheet } from 'react-native';
import {
  Pressable,
  GestureHandlerRootView,
} from 'react-native-gesture-handler';
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withTiming,
} from 'react-native-reanimated';

export default function EmptyExample() {
  const opacity = useSharedValue(0);

  const containerAnimatedStyle = useAnimatedStyle(() => {
    'worklet';
    return {
      opacity: opacity.value,
      transform: [{ scale: opacity.value }],
    };
  });

  useEffect(() => {
    opacity.value = withTiming(1, { duration: 200 });
  }, [opacity]);

  return (
    <GestureHandlerRootView style={styles.container}>
      <Animated.View style={containerAnimatedStyle}>
        <Pressable style={styles.box} onPress={() => console.log('press')} />
      </Animated.View>
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    gap: 10,
  },
  box: {
    width: 150,
    height: 150,
    backgroundColor: 'red',
    alignItems: 'center',
    justifyContent: 'center',
  },
});


```

</details>

---------

Co-authored-by: Michał Bert <63123542+m-bert@users.noreply.github.com>
## Description

As stated in #3611, docs are a bit unclear when it comes to explaining
usage of `anchor` and `focal` point in callbacks. Hopefully after this
PR they'll be less confusing.

## Test plan

Read docs 😄
## Description

Currently when we start `expo-example` it uses expo go by default. This
PR changes it do use `dev client` instead

## Test plan

Run `expo-example`
## Description
 Passing `enabled={false}` to buttons and pressable in jest tests now has effect. I
properly configured the corresponding mocks. Change was motivated by
issue #2385.

## Test plan
Tested using React Native 80.1
```ts
import { render, userEvent } from '@testing-library/react-native';
import { RectButton } from 'react-native-gesture-handler';


export const user = userEvent.setup()


describe('Testing disabled Button', () => {
  it('onPress does not trigger', async () => {
    const onPress = jest.fn();
    const { getByTestId } = render(
      <RectButton testID="btn" onPress={onPress} enabled={false} />
    );
    const btn = getByTestId('btn');

    expect(onPress).not.toHaveBeenCalled();
    await user.press(btn);
    expect(onPress).not.toHaveBeenCalled(); 
  });
});
```
## Description

Fixes #3608. On web, when gesture was active and its component had been
dropped, the gesture was not dropped. This caused conflicts with other
handlers, and prevented registering other gestures.
I added a method to drop handlers in the `GestureOchestrator` and call
it when component is dropped.


-->

## Test plan
```ts
import React from 'react';
import { useState } from 'react';
import { Pressable, TextInput } from 'react-native-gesture-handler';
export default function EmptyExample() {
  const [shown, setShown] = useState(true)

  if (!shown) {
    return (
      <Pressable
        key="1"
        testID="other-pressable"
        style={{ width: 30, height: 30, backgroundColor: 'red' }}
        onPress={() => console.log('pressed')}
      />
    )
  }

  return (
    <Pressable key="2" testID="bad-pressable" onPress={() => { }}>
      <TextInput
        style={{ backgroundColor: 'green', width: 100, height: 30 }}
        onSubmitEditing={() => setShown(false)}
      />
    </Pressable>
  )
};
```
Release 2.27.2

🚀
## Description

The link in the error message about the missing `GestureHandlerRootView`
was outdated, and the page no longer exists.

## Test plan

Open the new link
## Description

On web, In `EventManagers`, the `registerListeners` was called twice for
each handler of a `Button`

## Test plan

* Add a console.log to both `registerListners()` and
`unregisterListeners()` in `web/tools/KeyboardEventManager` (or any
other event manager)
* Observe logs in the `Web styles reset` example in the example app
## Description

This PR introduces integration with `Reanimated` to `NativeDetector`. When `Reanimated` is installed callbacks are handled via `useEvent` hook. Otherwise, they run on JS.

It also creates `v3` directory (name can be changed later) that will be used in further development to separate new code from the old one. 

## Test plan

Tested on basic-example app.
## Description

In #3617 we've merged `onUpdate` and `onChange` callbacks so `onUpdate` now contains information about changes (or better to say, it will when we implement it 😄). However, there are still places in code where you can see `onChange` being referenced. This PR cleans that up.

## Test plan

Tested on basic-example.
## Description

This PR automatically assigns `onUpdate` callback to `onGestureHandlerAnimatedEvent` when `Animated.Event` is passed. It also checks whether `change*` fields were used in `Animated.Event` - if so, we throw an error.

## Test plan

Since basic example is already "under construction", I'll copy whole code. Forgive me for I have sinned 🙏 

Basically it covers the case where we switch between `Animated.Event` and standard callback, along with using `change*` in `Animated.Event`.


<details>
<summary> Test code </summary>

```tsx
import * as React from 'react';
import { Animated, Button, useAnimatedValue } from 'react-native';
import {
  GestureHandlerRootView,
  NativeDetector,
  useGesture,
} from 'react-native-gesture-handler';

export default function App() {
  const [visible, setVisible] = React.useState(true);
  const [shouldUseAnimated, setShouldUseAnimated] = React.useState(true);

  const value = useAnimatedValue(0);
  const animatedEvent = Animated.event(
    [
      { nativeEvent: { handlerData: { translationX: value } } },
      // { nativeEvent: { handlerData: { changeX: 10 } } },
    ],
    {
      useNativeDriver: true,
    }
  );
  const jsCallback = (e: any) => console.log(e.nativeEvent);

  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
  const gesture = useGesture('PanGestureHandler', {
    // onGestureHandlerAnimatedEvent: event,
    // onGestureHandlerEvent: (e: any) =>
    //   console.log('onGestureHandlerEvent', e.nativeEvent),
    onUpdate: shouldUseAnimated ? animatedEvent : jsCallback,
    onEnd: (_e: any) => {
      setShouldUseAnimated((prev) => !prev);
    },

    changeEventCalculator: (event: any, _lastUpdateEvent: any) => {
      return { ...event.nativeEvent, changeX: 10 };
    },
  });

  return (
    <GestureHandlerRootView
      style={{ flex: 1, backgroundColor: 'white', paddingTop: 8 }}>
      <Button
        title="Toggle visibility"
        onPress={() => {
          setVisible(!visible);
        }}
      />

      {visible && (
        <NativeDetector gesture={gesture}>
          <Animated.View
            style={[
              {
                width: 150,
                height: 150,
                backgroundColor: 'blue',
                opacity: 0.5,
                borderWidth: 10,
                borderColor: 'green',
                marginTop: 20,
                marginLeft: 40,
              },
              { transform: [{ translateX: value }] },
            ]}
          />
        </NativeDetector>
      )}
    </GestureHandlerRootView>
  );
}

```

</details>
## Description

#3630 introduced automatic support for `Animated.Event` in `onUpdate` callback. However, I've missed the fact that this callback may not be defined. This PR fixes this crash.

## Test plan

Tested on current basic-example without specifying `onUpdate`
## Description

We came to a conclusion that current event handling may conflict with gesture relations, such as `simultaneous` when one of the gestures is using `Animated`. This PR removes `AnimatedEvent` action type from `NativeDetector` and adds flag to `GestureHandler`, which represents if handler should send events for `Animated`.

## Status 

- ### Android ✅ 
- ### iOS ✅ 

## Test plan

Test `pan1`, `pan2` and `gesture` from the code below.

>[!NOTE]
> Since `simultaneous` relation is not yet implemented, only first pan works when `gesture` is used. On Android you can solve that ba commenting out `otherHandler.cancel()` in `makeActive` function ([this line](https://github.com/software-mansion/react-native-gesture-handler/blob/0e5d58efcb2bd0ee2bd4eefa78fd366e003eea41/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt#L205))

<details>
<summary>Tested on the following code:</summary>

```tsx
import * as React from 'react';
import { Animated, Button, useAnimatedValue } from 'react-native';
import {
  GestureHandlerRootView,
  NativeDetector,
  useGesture,
} from 'react-native-gesture-handler';

export default function App() {
  const [visible, setVisible] = React.useState(true);

  const value = useAnimatedValue(0);
  const event = Animated.event(
    [{ nativeEvent: { handlerData: { translationX: value } } }],
    {
      useNativeDriver: true,
    }
  );

  const pan1 = useGesture('PanGestureHandler', {
    onUpdate: (e: any) => {
      console.log('Pan1 update:', e);
    },
  });

  const pan2 = useGesture('PanGestureHandler', {
    onUpdate: event,
    dispatchesAnimatedEvents: true,
  });

  const gesture = {
    tag: [pan1.tag, pan2.tag],
    gestureEvents: {
      onGestureHandlerStateChange: (e) => {
        pan1.gestureEvents.onGestureHandlerStateChange(e);
        pan2.gestureEvents.onGestureHandlerStateChange(e);
      },
      onGestureHandlerEvent: pan1.gestureEvents.onGestureHandlerEvent,
      onGestureHandlerAnimatedEvent:
        pan2.gestureEvents.onGestureHandlerAnimatedEvent,
      onGestureHandlerTouchEvent: (e) => {
        pan1.gestureEvents.onGestureHandlerTouchEvent(e);
        pan2.gestureEvents.onGestureHandlerTouchEvent(e);
      },
    },
    dispatchesAnimatedEvents: true, // Assuming this is always true for this example
  };

  return (
    <GestureHandlerRootView
      style={{ flex: 1, backgroundColor: 'white', paddingTop: 8 }}>
      <Button
        title="Toggle visibility"
        onPress={() => {
          setVisible(!visible);
        }}
      />

      {visible && (
        <NativeDetector gesture={gesture}>
          <Animated.View
            style={[
              {
                width: 150,
                height: 150,
                backgroundColor: 'blue',
                opacity: 0.5,
                borderWidth: 10,
                borderColor: 'green',
                marginTop: 20,
                marginLeft: 40,
              },
              { transform: [{ translateX: value }] },
            ]}
          />
        </NativeDetector>
      )}
    </GestureHandlerRootView>
  );
}
```

</details>
## Description

#3646 removed _animated native detector_ action type. However, now we have to pass `dispatchesAnimatedEvents` to gesture config, what was not done automatically. This PR fixes it.

## Test plan

Tested on `basic-example`
## Description


#3646
removed the Animated action type, but there are still references left to
`dispatchesAnimatedEvents` in the native detector, while the property
has been moved to the config of individual gestures.

This PR removes those references.

## Test plan

Check the native detector examples
## Description

This PR brings support for `NativeViewGestureHandler` into `NativeDetector`. It does so, by attaching handler into child instead of detector view. 

## Status

- ### Android ✅ 
- ### iOS ✅ 

## Test plan

<details>
<summary>Tested on the following code:</summary>

```jsx
import * as React from 'react';
import { StyleSheet, Text, View, ScrollView } from 'react-native';
import {
  GestureHandlerRootView,
  NativeDetector,
  useGesture,
} from 'react-native-gesture-handler';

export default function App() {
  const items = Array.from({ length: 30 }, (_, index) => `Item ${index + 1}`);

  const gesture = useGesture('NativeViewGestureHandler', {
    onBegin: (e) => {
      console.log('onBegin', e);
    },
    onStart: (e) => {
      console.log('onStart', e);
    },
    onEnd: (e) => {
      console.log('onEnd', e);
    },
    onFinalize: (e) => {
      console.log('onFinalize', e);
    },
    onTouchesDown: (e) => {
      console.log('onTouchesDown', e);
    },
    onTouchesMove: (e) => {
      console.log('onTouchesMove', e);
    },
    onTouchesCancelled: (e) => {
      console.log('onTouchesCancelled', e);
    },
    onTouchesUp: (e) => {
      console.log('onTouchesUp', e);
    },
    onUpdate: (e) => {
      console.log('onUpdate', e);
    },
    onChange: (e) => {
      console.log('onChange', e);
    },
    onCancel: (e) => {
      console.log('onCancel', e);
    },
  });

  return (
    <GestureHandlerRootView
      style={{ flex: 1, backgroundColor: 'white', paddingTop: 8 }}>
      <NativeDetector gesture={gesture}>
        <ScrollView style={styles.scrollView}>
          {items.map((item, index) => (
            <View key={index} style={styles.item}>
              <Text style={styles.text}>{item}</Text>
            </View>
          ))}
        </ScrollView>
      </NativeDetector>
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  scrollView: {
    backgroundColor: 'lightgrey',
    marginHorizontal: 20,
  },
  item: {
    flexDirection: 'row',
    justifyContent: 'center',
    alignItems: 'center',
    padding: 20,
    margin: 2,
    backgroundColor: 'white',
    borderRadius: 10,
  },
  text: {
    fontSize: 20,
    color: 'black',
  },
});
```

</details>
## Description

This PR adds a web version of `NativeDetector` component following its
IOS/android implementations.

## Test plan
* copy all react source files from basic-example to expo-example
* add a console log, in the `NativeDetector` pan Gesture
* check whether the gesture is properly recognised
m-bert and others added 26 commits January 29, 2026 13:28
## Description

Currently if `Reanimated` is not installed, running apps on `iOS`
results in redbox that says that it `Reanimated` module doesn't exist.

It happens because in [this
line](https://github.com/software-mansion/react-native-gesture-handler/blob/6825dbbd07bb73730a7c23ab140f96c00b93bb8d/packages/react-native-gesture-handler/apple/RNGestureHandlerModule.mm#L125)
in module we call `[self.moduleRegistry
moduleForName:"ReanimatedModule"]`. This goes through
[RCTModuleRegistry](https://github.com/facebook/react-native/blob/ca520ee4c19dd921f9225c71a9ad872c4bbd7cb9/packages/react-native/React/Base/RCTModuleRegistry.m#L50)
to
[RCTTurboModuleManager](https://github.com/facebook/react-native/blob/ca520ee4c19dd921f9225c71a9ad872c4bbd7cb9/packages/react-native/ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModuleManager.mm#L980),
where this method is forced to throw redbox if module is not found.

To fix this, we move check for `Reanimated` availability to JS side.

## Test plan

Tested on basic-example app with and without `Reanimated`
## Description

Context menu was broken on v3, this PR fixes it. 

WebDelegate never handled changing contextMenu. when context had been
disabled the `areContextMenuListenersAdded` was set to true, later when
it was enabled it would not change the listener from disabled to enabled
as the `addContextMenuListeners` returned after seeing that the
listeners had already been added.

## Test plan

Tested on the following example:

<details>

```tsx

import React from 'react';
import { StyleSheet, View } from 'react-native';
import {
  GestureDetector,
  MouseButton,
  usePanGesture,
} from 'react-native-gesture-handler';

export default function ContextMenuExample() {
  const p1 = usePanGesture({ mouseButton: MouseButton.RIGHT });
  const p2 = usePanGesture({});
  const p3 = usePanGesture({});
  return (
    <View style={styles.container}>
      <GestureDetector gesture={p1}>
        <View style={[styles.box, styles.grandParent]}>
          <GestureDetector gesture={p2} enableContextMenu={true}>
            <View style={[styles.box, styles.parent]}>
              <GestureDetector gesture={p3} enableContextMenu={false}>
                <View style={[styles.box, styles.child]} />
              </GestureDetector>
            </View>
          </GestureDetector>
        </View>
      </GestureDetector>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'space-around',
    alignItems: 'center',
  },

  grandParent: {
    width: 300,
    height: 300,
    backgroundColor: "navy",
  },

  parent: {
    width: 200,
    height: 200,
    backgroundColor: "purple",
  },

  child: {
    width: 100,
    height: 100,
    backgroundColor: "blue",
  },

  box: {
    display: 'flex',
    justifyContent: 'space-around',
    alignItems: 'center',
    borderRadius: 20,
  },
});

```

</details>
## Description

The new `StateManager` is global and given `handlerTag` it can manually
set the states of an arbitrary gesture. This causes errors, when the
gesture, which state is being set, has not been yet recorded in the
orchestrator. Recording gestures in the orchestrator on android is done
lazily, thus if it never received touches it is not recorded.

It also adds explicit error when trying to manually handled a gesture
not attached to any detector on all platforms.

## Test plan

Tested on the following example
<details>

```ts
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { GestureHandlerRootView, GestureDetector, useLongPressGesture, GestureStateManager, usePanGesture, useSimultaneousGestures, useTapGesture } from 'react-native-gesture-handler';

export default function TwoPressables() {
  const longPress = useLongPressGesture({
    onTouchesDown: (e) => {
      'worklet';
      console.log("touches down")
    },
    onActivate: () => {
      'worklet';
      console.log("long pressed")
    },
    minDuration: 100000000,
    disableReanimated: true
  })
  const pan = useTapGesture({
    onTouchesDown: () => {
      'worklet';
      console.log("tap")

      GestureStateManager.activate(longPress.tag)
    },
    disableReanimated: true
  });
  return (
    <GestureHandlerRootView>
      <View style={styles.root}>
        <GestureDetector gesture={longPress}>
          <View style={styles.outer}>
            <Text style={styles.label}>Long Press</Text>
          </View>
        </GestureDetector>
        <GestureDetector gesture={pan}>
          <View style={styles.outer}>
            <Text style={styles.label}>Pan</Text>
          </View>
        </GestureDetector>

      </View>
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  root: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#f7f7f7',
  },
  outer: {
    padding: 20,
    backgroundColor: '#ddd',
    borderRadius: 12,
    marginBottom: 50
  },
  label: {
    fontSize: 18,
    marginBottom: 10,
  },
})
```

</details>
#3936)

## Description

Gesture detection on complex views (like Text) relies on
`gestureRecognizerShouldBegin` checking for the virtual react tag. The
logic itself is fine, but iOS runs it when it wants to see if the
gesture should activate, at which point all gestures attached to the
composed view have fired their `onBegin` callbacks.

This PR explicitly calls this method inside
`onTouches(Interactions)Began` to immediately fail handlers that don't
pass the test.

## Test plan

Test the `basic-example/Text` with `onBegin` instead of `onActivate`.
…3884)

Adds an explicit error message when `GestureDetector` is rendered
without any gesture. The current behavior is a random error when trying
to call a method on `undefined`.

Verify that the error is thrown when no gesture is passed.
…rack pad (#3865)

Currently the track pad pinch gesture returns the number of touch points
to 0 from native method. This causes the focal X and Y to become NaN.
This fixes the issue reported in
#3864
## Description

In #3855 we've introduced `fromReset` argument for `handleGesture`.
However, `ManualActivationRecognizer` does not have such signature. This
PR removes it from selector as `fromReset` is not passed anyway.

## Test plan

Check that on `Pressable` example app no longer crashes when clicking on
**press retention** area.
## Description

Adds a banner linking to the State of React Native Survey in readme.
## Description

Removes a banner linking to the State of React Native Survey from
readme.
## Description

fixes:
#3891
fixes:
#3904


This PR implements `pointerEvents` support for `Pressable` component
from `react-native-gesture-handler` on iOS. Previously, setting
`pointerEvents="box-none"` (or other modes) had no effect on iOS, while
it worked correctly <s>on Android</s> and with React Native's
`Pressable` on iOS.

Android PR:
#3927

### Implementation Details

The implementation follows React Native's `hitTest` logic for
`pointerEvents`:
- For `box-only`: Returns `self` if point is inside (respecting
`hitSlop`), `nil` otherwise
- For `box-none`: Checks subviews only, returns the hit subview or `nil`
- For `none`: Always returns `nil`
- For `auto`: Uses standard hit testing with `shouldHandleTouch` logic

The implementation respects `hitTestEdgeInsets` (hitSlop) for all modes,
ensuring consistent behavior with React Native's `Pressable`.

## Test plan

Tested all `pointerEvents` modes on iOS:
- ✅ `pointerEvents="none"` - View and subviews don't receive touches
- ✅ `pointerEvents="box-none"` - View doesn't receive touches, subviews
do
- ✅ `pointerEvents="box-only"` - View receives touches, subviews don't
- ✅ `pointerEvents="auto"` - Default behavior works as expected

I've used https://github.com/huextrat/repro-pressable-gh to test
scenarios

Tested on both old architecture (Paper) and new architecture (Fabric).

edit: `pointerEvents` with RNGH Pressable is not working on Android

---------

Co-authored-by: Michał <michal.bert@swmansion.com>
fixes:
#3891
fixes:
#3904

This PR fixes `pointerEvents` support for `Pressable` component from
`react-native-gesture-handler` on Android.

Waiting for iOS PR:
#3925
as codegen is involved and a small iOS changes is needed

Tested all `pointerEvents` modes on Android:
- ✅ `pointerEvents="none"` - View and subviews don't receive touches
- ✅ `pointerEvents="box-none"` - View doesn't receive touches, subviews
do
- ✅ `pointerEvents="box-only"` - View receives touches, subviews don't
- ✅ `pointerEvents="auto"` - Default behavior works as expected

I've used https://github.com/huextrat/repro-pressable-gh to test
scenarios

Tested on both old architecture (Paper) and new architecture (Fabric).
## Description

Updates the `set-package-version` script to also support beta and
release candidate versions. Since those two may not be published from a
stable branch, they require explicit version to be set.

The version format for those releases would be:
```
{major}.{minor}.{patch}-rc.{rcVersion}
{major}.{minor}.{patch}-beta.{betaVersion}
```

## Test plan

Run the script in different configurations

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
## Description

Updates mocks to mock `HostGestureDetector`, and have a separate file
for mocking buttons. Moreover it adds tests for correct error throws in
V3.

## Test plan

`yarn test`
## Description

On android and web gestures activated with state manager were not
cleaned up properly as they were never registered in the gesture
orchestrator.

## Test plan

Tested on the following example

<details>

```tsx

import React from 'react';
import { StyleSheet, View } from 'react-native';
import {
  GestureHandlerRootView,
  GestureDetector,
  useLongPressGesture,
  GestureStateManager,
  LongPressGesture,
} from 'react-native-gesture-handler';
import Animated, {
  useAnimatedStyle,
  useSharedValue,
  withTiming,
} from 'react-native-reanimated';

export const COLORS = {
  offWhite: '#f8f9ff',
  headerSeparator: '#eef0ff',
  PURPLE: '#b58df1',
  NAVY: '#001A72',
  RED: '#A41623',
  YELLOW: '#F2AF29',
  GREEN: '#0F956F',
  GRAY: '#ADB1C2',
  KINDA_RED: '#FFB2AD',
  KINDA_YELLOW: '#FFF096',
  KINDA_GREEN: '#C4E7DB',
  KINDA_BLUE: '#A0D5EF',

};
export default function TwoPressables() {
  const isActivated = [
    useSharedValue(0),
    useSharedValue(0),
    useSharedValue(0),
    useSharedValue(0),
  ];
  const gestures: LongPressGesture[] = [];

  const createGestureConfig = (index: number) => ({
    onActivate: () => {
      'worklet';
      isActivated[index].value = 1;
      console.log(`Box ${index}: long pressed`);

      const nextIndex = index + 1;
      if (nextIndex < gestures.length) {
        const nextGesture = gestures[nextIndex];
        if (nextGesture) {
          GestureStateManager.activate(nextGesture.handlerTag);
        }
      }
    },
    onFinalize: () => {
      'worklet';
      isActivated[index].value = 0;
      const nextIndex = index + 1;
      if (nextIndex < gestures.length) {
        const nextGesture = gestures[nextIndex];
        if (nextGesture) {
          GestureStateManager.deactivate(nextGesture.handlerTag);
        }
      }
    },
    disableReanimated: true,
  });

  const g0 = useLongPressGesture(createGestureConfig(0));
  const g1 = useLongPressGesture(createGestureConfig(1));
  const g2 = useLongPressGesture(createGestureConfig(2));
  const g3 = useLongPressGesture(createGestureConfig(3));

  gestures[0] = g0;
  gestures[1] = g1;
  gestures[2] = g2;
  gestures[3] = g3;

  const colors = [COLORS.PURPLE, COLORS.NAVY, COLORS.GREEN, COLORS.RED];

  function Box({ index }: { index: number }) {
    const animatedStyle = useAnimatedStyle(() => ({
      opacity: isActivated[index].value === 1 ? 0.5 : 1,
      transform: [
        { scale: withTiming(isActivated[index].value === 1 ? 0.95 : 1) },
      ],
    }));

    return (
      <GestureDetector gesture={gestures[index]}>
        <Animated.View
          style={[
            commonStyles.box,
            { backgroundColor: colors[index] },
            animatedStyle,
          ]}
        />
      </GestureDetector>
    );
  }
  return (
    <GestureHandlerRootView>
      <View style={commonStyles.centerView}>
        <Box index={0} />
        <Box index={1} />
        <Box index={2} />
        <Box index={3} />
      </View>
    </GestureHandlerRootView>
  );
}
const commonStyles = StyleSheet.create({
  centerView: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  box: {
    height: 150,
    width: 150,
    borderRadius: 20,
    marginBottom: 30,
  },
})

```
## Description

Addresses the underlying issue of
#3906

The original issue described in the above PR was caused by wrong shadow
node dimensions, which were fixed by
#3930.
After that, the dimensions were good, but the long press was still
failing. This was caused by `shouldCancelWhenOutside` checking the
dimensions of the detector while the child was moved.

This PR changes the logic so that the child's hitbox is checked when
using the native detector. This should be enough for iOS, but on
Android, further investigation is needed into whether the entire
`transformedEvent` should be in the coordinate space of the detector or
its child.

## Test plan

See
#3906
test plan
## Description

This PR handles state management in our docs:

- Moves information about states to "under the hood" section
- Rewrites entire **states & events** page to focus more on callbacks
- Updates `GestureStateManager` entry and moves it to **fundamentals**
- Merges manual gesture guide with manual gesture docs

## Test plan

Read docs 🤓

---------

Co-authored-by: Jakub Piasecki <jakub.piasecki@swmansion.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
## Description

This PR removes old migration guide and adds new one.

## Test plan

Read docs 🤓

---------

Co-authored-by: Jakub Piasecki <jakub.piasecki@swmansion.com>
Co-authored-by: Andrzej Antoni Kwaśniewski <81448793+akwasniewski@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
## Description

Though we allowed `HitSlop` to be `SharedValue` in gesture config, we
excluded it from being split. Therefore if someone did something like:

```ts
const sv = useSharedValue(10);

const pan = usePanGesture({
  hitSlop: sv;
})
```

this wouldn't work as `SharedValue<number>` couldn't be assigned to
`SharedValue<HitSlop>`.

This PR changes behavior of `SharedValueOrT<T>` type, so that it uses
[Distributive Conditional
Types](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types)
to split union into separate types and then apply `SharedValue`.

## Test plan

`yarn ts-check` and `yarn lint-js`

<details>
<summary>Tested on the following code:</summary>

```ts
import { useSharedValue } from 'react-native-reanimated';

import {
  ActiveCursor,
  MouseButton,
  useHoverGesture,
  usePanGesture,
  HoverEffect,
} from 'react-native-gesture-handler';

export function App() {
  const runOnJS_SV = useSharedValue(true);
  const enabled_SV = useSharedValue(false);
  const shouldCancelWhenOutside_SV = useSharedValue(true);
  const hitSlop_SV = useSharedValue(10);
  const activeCursor_SV = useSharedValue<ActiveCursor>('pointer');
  const mouseButton_SV = useSharedValue(MouseButton.LEFT);
  const cancelsTouchesInView_SV = useSharedValue(true);
  const manualActivation_SV = useSharedValue(false);

  const minDistance_SV = useSharedValue(5);
  const offsetStart_SV = useSharedValue(0);
  const hoverEffect_SV = useSharedValue(HoverEffect.LIFT);

  const pan1 = usePanGesture({
    runOnJS: false,
    enabled: true,
    shouldCancelWhenOutside: true,
    hitSlop: 10,
    activeCursor: 'pointer',
    mouseButton: MouseButton.LEFT,
    cancelsTouchesInView: true,
    manualActivation: false,

    minDistance: 20,
    activeOffsetX: [10, 20],
  });

  const pan2 = usePanGesture({
    runOnJS: runOnJS_SV,
    enabled: enabled_SV,
    shouldCancelWhenOutside: shouldCancelWhenOutside_SV,
    hitSlop: hitSlop_SV,
    activeCursor: activeCursor_SV,
    mouseButton: mouseButton_SV,
    cancelsTouchesInView: cancelsTouchesInView_SV,
    manualActivation: manualActivation_SV,

    minDistance: minDistance_SV,
    activeOffsetX: [offsetStart_SV, 20],
  });

  const hover1 = useHoverGesture({
    effect: HoverEffect.LIFT,
  });

  const hover2 = useHoverGesture({
    effect: hoverEffect_SV,
  });

  console.log(pan1, pan2, hover1, hover2);
}
```

</details>
## Description

Made a new PR, as it is easier than attempt to merge the [old
one](#3725)
with next.

This PR adds a toggle in the example app to switch between Legacy and V3
api examples. In the new api section I added split into few sections.
Most of them are rewritten old V2 examples with more consistent styling.
 I also added a Feedback component to showcase features console free. 
The new examples are:
* basic examples for every gestures
* lock to showcase combining gestures
* sharedValue, nestedText, svg - to showcase VirtualDetector 
* animated - to showcase cooperation with animated

## Test plan

See if the examples in new api section from common-example work
## Description

Removes failing tests for the V1 API. Given that it has been deprecated
for quite a while, and there are plans to remove it, I feel like getting
rid of the tests is justified. It will also enable us to run the tests
continuously on GitHub Actions.

## Test plan

Run the tests
## Description

Overhaul of the release process:
- adds `beta` and `rc` options to the previously existing `stable` and
`commitly`
- allows the release of a specific version, independent from the branch
name (optional, will still detect the version from the branch name when
unspecified)
- verifies that the latest version is either one patch, one minor, or
one major higher than the currently published version (ATM throws on
major change)
- verifies that beta, rc and commitly releases aren't done for an
already published version (i.e. will disallow publishing `2.30.0-beta.1`
when stable `2.30.0` is published`).
- automatic numbering of beta and rc releases

Internally:
- moved most of the helpers to separate files
- moved version numbering helpers to `version-utils`
- added tests covering the release scripts

## Test plan

Tested on a fork:
- Fails when trying to skip minor on latest release:
https://github.com/j-piasecki/react-native-gesture-handler/actions/runs/21475940199/job/61859862583
- Fails when trying to skip patch on latest release:
https://github.com/j-piasecki/react-native-gesture-handler/actions/runs/21476045075/job/61860214823
- Fails when trying to publish on existing versions (checks base version
for beta, rc, and commitly):
https://github.com/j-piasecki/react-native-gesture-handler/actions/runs/21476741044/job/61862557674
- Succeeds when not skipping any minor:
https://github.com/j-piasecki/react-native-gesture-handler/actions/runs/21476056662/job/61860252565
- Succeeds when not skipping any patch:
https://github.com/j-piasecki/react-native-gesture-handler/actions/runs/21476183083/job/61860683061
- Succeeds when publishing a non-latest release:
https://github.com/j-piasecki/react-native-gesture-handler/actions/runs/21476510871/job/61861782101
Bumps
[github-pages-deploy-action](https://github.com/JamesIves/github-pages-deploy-action)
to v4. Previously, the action inconsistently pulled either the latest
commit or its predecessor.

Run the action and check whether it pulled latest commit
## Description

This PR does two things:

1. Renames `onTouchesCancelled` callback to `onTouchesCancel`
2. Adds few information to migration guide:
  2.1. Added information that `onChange` was removed
  2.2. Added information about removed `state`, `oldState`
  2.3. Added information about unified callback events types. 

## Test plan

Read docs 🤓
## Description

`InterceptingDetectorProps` was missing some types from
`GestureDetectorProps` this PR makes both types extend
`CommonGestureDetectorProps`

## Test plan

`yarn ts-check`

---------

Co-authored-by: Jakub Piasecki <jakub.piasecki@swmansion.com>
## Description

Temporarily disables commitly releases.
Copilot AI review requested due to automatic review settings February 5, 2026 09:46
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR merges the Gesture Handler 3 development branch (next) into main, introducing significant architectural changes and a new API for gesture handling in React Native applications.

Changes:

  • Removes Paper-specific generated code and transitions to Fabric-only architecture
  • Introduces new Gesture Handler 3 API with hook-based gesture composition
  • Updates documentation structure to support versioned docs (2.x and 3.x)

Reviewed changes

Copilot reviewed 232 out of 604 changed files in this pull request and generated no comments.

Show a summary per file
File Description
packages/react-native-gesture-handler/android/paper/src/main/java/com/facebook/react/viewmanagers/*.java Removes Paper architecture generated interface and delegate files
packages/react-native-gesture-handler/android/noreanimated/src/main/java/com/swmansion/gesturehandler/ReanimatedProxy.kt Renames class from ReanimatedEventDispatcher to ReanimatedProxy
packages/react-native-gesture-handler/android/build.gradle Removes Paper/Fabric conditional compilation and architecture checks
packages/react-native-gesture-handler/android/CMakeLists.txt Adds CMake configuration for Fabric codegen compilation
packages/react-native-gesture-handler/{Swipeable,DrawerLayout}/package.json Removes deprecated component package.json files
packages/react-native-gesture-handler/RNGestureHandler.podspec Updates source files to include shared C++ code and simplifies dependency configuration
packages/docs-gesture-handler/versions.json Adds version 2.x to documentation versions
packages/docs-gesture-handler/versioned_docs/version-2.x/**/*.md Adds versioned documentation for Gesture Handler 2.x API
packages/docs-gesture-handler/src/** Updates documentation components and styling for versioned docs support
apps/** Updates example apps to use new API and removes deprecated examples
.github/workflows/** Removes Paper-specific CI workflows and adds GH3 API testing workflow

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown
Collaborator

@m-bert m-bert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bye bye old API 👋

🚀🚀🚀

@j-piasecki j-piasecki merged commit 77fa524 into main Feb 5, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.