Skip to content

Cherry-pick bug fixes and improvements from main to v2-stable#4053

Closed
Copilot wants to merge 323 commits intov2-stablefrom
copilot/update-pull-request-list
Closed

Cherry-pick bug fixes and improvements from main to v2-stable#4053
Copilot wants to merge 323 commits intov2-stablefrom
copilot/update-pull-request-list

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 2, 2026

⚠️ IMPORTANT: Please change the base branch of this PR to v2-stable ⚠️

The PR was auto-created targeting main, but it should target v2-stable. Please click "Edit" next to the base branch and change it to v2-stable. The diff will then show only the 33 files with cherry-picked changes.


Description

This PR cherry-picks 14 PRs from main to v2-stable, sorted by merge date. V3-specific changes (files under src/v3/, src/specs/RNGestureHandlerDetectorNativeComponent.ts, etc.) were excluded during conflict resolution since they don't apply to the v2 API.

Cherry-picked PRs (in merge-date order):

  1. Fix GestureDetector unresponsive after display:none toggle (New Arch) #3964 - Fix GestureDetector unresponsive after display:none toggle (New Arch)
  2. [iOS] Add wrapper for handleGesture:fromReset: #3983 - [iOS] Add wrapper for handleGesture:fromReset:
  3. Change handler reference to weak pointer to prevent memleaks #3987 - Change handler reference to weak pointer to prevent memleaks
  4. [iOS] Fix wrong pointerType in Hover gesture #3989 - [iOS] Fix wrong pointerType in Hover gesture
  5. Memoize Text component #4003 - Memoize Text component
  6. [iOS] Distinguish between mouse and stylus when hovering #3991 - [iOS] Distinguish between mouse and stylus when hovering
  7. [Android] Check all pointers in move events #4010 - [Android] Check all pointers in move events
  8. Move @types/react-test-renderer to dependencies #4015 - Move @types/react-test-renderer to dependencies
  9. Enable exactOptionalPropertyTypes support #4012 - Enable exactOptionalPropertyTypes support (V3-only files skipped)
  10. [Android] Clear blocking relations on drop #4020 - [Android] Clear blocking relations on drop (already on v2-stable, skipped)
  11. [iOS] Add numberOfPointers to Native gesture events #4023 - [iOS] Add numberOfPointers to Native gesture events
  12. [iOS] Fix missing onFinalize callback in Tap #4029 - [iOS] Fix missing onFinalize callback in Tap
  13. [iOS] Handle FullWindowOverlay as the native root #4039 - [iOS] Handle FullWindowOverlay as the native root (already on v2-stable, skipped)
  14. fix(ios): guard +load with #ifdef RCT_DYNAMIC_FRAMEWORKS #4047 - fix(ios): guard +load with #ifdef RCT_DYNAMIC_FRAMEWORKS

Notes on conflict resolution:

Test plan

Individual PRs were tested on their respective platforms. This cherry-pick preserves the same changes adapted for the v2 API surface.

akwasniewski and others added 30 commits September 29, 2025 14:52
## Description

After
#3715
v3 api relations did not work. This PR fixes this issue, now relations
work in both APIs.

## Test plan
<details> 

<summary>old api</summary>

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

export default function Example() {
  const innerTap = Gesture.Tap()
    .onStart(() => {
      console.log('inner tap');
    });

  const outerTap = Gesture.Tap()
    .onStart(() => {
      console.log('outer tap');
    })
    .simultaneousWithExternalGesture(innerTap);

  return (
    <GestureHandlerRootView style={styles.container}>
      <GestureDetector gesture={outerTap}>
        <View style={styles.outer}>
          <GestureDetector gesture={innerTap}>
            <View style={styles.inner} />
          </GestureDetector>
        </View>
      </GestureDetector>
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
  outer: {
    width: 250,
    height: 250,
    backgroundColor: 'lightblue',
  },
  inner: {
    width: 100,
    height: 100,
    backgroundColor: 'blue',
    alignSelf: 'center',
  },
});
```

</details>

<details>

<summary>new api</summary>

```ts
import * as React from 'react';
import { Animated, Button, useAnimatedValue } from 'react-native';
import {
  GestureHandlerRootView,
  NativeDetector,
  useSimultaneous,
  useGesture,
  useExclusive,
  useRace,
  SingleGestureName,
} from 'react-native-gesture-handler';

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

  const av = React.useRef(new Animated.Value(0)).current
  const event = Animated.event(
    [{ nativeEvent: { handlerData: { translationX: av } } }],
    {
      useNativeDriver: true,
    }
  );

  const tap1 = useGesture(SingleGestureName.Tap, {
    onEnd: () => {
      // 'worklet';
      console.log('Tap 1');
    },
    numberOfTaps: 1,
    disableReanimated: true,

  });

  const tap2 = useGesture(SingleGestureName.Tap, {
    onEnd: () => {
      // 'worklet';
      console.log('Tap 2');
    },
    numberOfTaps: 2,
    disableReanimated: true,
  });

  // const tap1 = useGesture('TapGestureHandler', {
  //   onEnd: () => {
  //     'worklet';
  //     console.log('Tap 1');
  //   },
  //   numberOfTaps: 1,
  //   // disableReanimated: true,
  //   requireExternalGestureToFail: tap2,
  // });

  // const pan1 = useGesture(SingleGestureName.Pan, {
  //   // onUpdate: event,
  //   onUpdate: (e) => {
  //     'worklet';
  //     console.log('Pan 1');
  //   },
  //   // disableReanimated: true,
  // });
  //
  // const pan2 = useGesture(SingleGestureName.Pan, {
  //   onUpdate: (e) => {
  //     'worklet';
  //     console.log('Pan 2');
  //   },
  //   simultaneousWithExternalGesture: pan1,
  //   // requireExternalGestureToFail: pan1,
  //   // blocksExternalGesture: pan1,
  //   // disableReanimated: true,
  // });

  // const composedGesture = useSimultaneous(pan1, pan2);

  const composedGesture = useExclusive(tap2, tap1);
  // const composedGesture = useExclusive(pan2, pan1); // For Animated.Event
  // const composedGesture = useExclusive(pan1, pan2); // For Animated.Event

  // const composedGesture = useRace(pan1, pan2);
  // const composedGesture = useRace(pan2, pan1);

  // const composedGesture = useExclusive(tap1, useSimultaneous(pan1, pan2));

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

      {visible && (
        <NativeDetector gesture={composedGesture}>
          <Animated.View
            style={[
              {
                width: 150,
                height: 150,
                backgroundColor: 'blue',
                opacity: 0.5,
                borderWidth: 10,
                borderColor: 'green',
                marginTop: 20,
                marginLeft: 40,
                display: 'flex',
                alignItems: 'center',
                justifyContent: 'space-around',
              },
              { transform: [{ translateX: av }] },
            ]}>
          </Animated.View>
        </NativeDetector>
      )}
    </GestureHandlerRootView>
  );
}
```
</details>
## Description

This PR removes the following warning when building the app in release
mode for Android:

```
> Task :react-native-gesture-handler:compileReleaseKotlin
w: file:///Users/tomekzaw/(...)/node_modules/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerRootView.kt:41:43 The corresponding parameter in the supertype 'ReactViewGroup' is named 'ev'. This may cause problems when calling this function with named arguments.
```

## Test plan

<!--
Describe how did you test this change here.
-->
## Description

This PR fixes the `Pressable`'s `onPress` callback being called despite
the press being cancelled.

Credit for finding and fixing this issue goes to @gigobyte.

Fixes #3609

## Test plan

- Run the following example:
https://gist.github.com/gigobyte/5d1d0bf9e5f188f6590210a21fc6eb3a
- Swipe the pink `Pressable` to the right.
- Before this PR, the `onPress` callback would be called without the
finger being lifted.
- Since this PR, the `onPress` is no longer called when the finger isn't
lifted.
In `metro` 0.83.2 `exclusionList` was changed to `default` export (see [this commit](facebook/metro@8610b2c#diff-a6e36af38cd6fde5360c601ce8f2f02e4e6b350b7fea9b0f049ab63f3b8f5709)). This means that we have to update our configs, or otherwise we won't be able to start server.

> [!NOTE]
> `macos-example` uses older versions of `react-native` so we don't have to update its config.

1. `yarn clean`
2. `yarn`
3. `yarn start` in both examples
…ble (#3666)

**Summary**
Fixes `SwipeDirection` being `undefined` at runtime in
`ReanimatedSwipeable` when the package
is consumed from TypeScript source (e.g. in some Metro configs).

**Details**
`SwipeDirection` was imported from the local barrel (`./index.ts`),
which re-exports it
as a type-only export. This removes the value in the compiled JS,
causing it to be `undefined`
at runtime.

This PR imports `SwipeDirection` directly from
`ReanimatedSwipeableProps`, which exports it
as a value.

See closed issue for details:
[#3665](#3665)

**Impact**
No API changes. Only affects internal import, ensures enum value is
present at runtime.

---------

Co-authored-by: Christian Bach <christian.bach@carnegie.se>
## Description

This PR simplifies logic that handles `mouseButton` property. I've changed `event.button` to `event.buttons`. These values now correctly align with our `MouseButton` enum, so `Map` is no longer necessary. 

| Button | `event.button` | `MouseButton` | `event.buttons` |
|--------|--------------|-------------|---------------|
| Left   | 0            | 1           | 1             |
| Right  | 2            | 2           | 2             |
| Middle | 1            | 4           | 4             |
| Btn_4  | 3            | 8           | 8             |
| Btn_5  | 4            | 16          | 16            |


You can find values in MDN docs for:

- `event.button` [here](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button#value)
- `event.buttons` [here](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons#value)

> [!NOTE]
> `event.buttons` also performs logical `or` operation on buttons, so for example using both **left** and **right** buttons at the same time results in value of `3`. 

> [!IMPORTANT]
> I've tested behavior when trying to activate handler with different buttons. In case where handler has `mouseButton(MouseButton.LEFT)` clicking first with left button, then adding other button results in activation. This is fine and aligns with what we have now. Clicking with other button and then adding left button does not result in activation. This is also fine.
>
> Now, clicking with both buttons at the same time does not result in activation. This is because in that case there's no `pointerdown` event. Turns out that `pointerdown` is called only for the first button, all the other pointers result in `pointermove` event. In case where both are pressed at the same time, there's only `pointermove` event.

## Test plan

Tested on **MouseButtons** example
## Description

This PR adds `name` property to handlers on web, so that we can avoid checks like in `InteractionsManager`:

```js
   const isNativeHandler =
      otherHandler.constructor.name === 'NativeViewGestureHandler';
```

## Test plan

Tested on **Swipeable** example on mobile version in chrome.
## Description

This PR adds `changeEventCalculator` functions into continuous handlers.

## Test plan

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

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

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

  const gesture = usePan({
    onUpdate: (e) => {
      'worklet';
      console.log(e.handlerData.changeX);
    },
  });

  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,
              },
            ]}
          />
        </NativeDetector>
      )}
    </GestureHandlerRootView>
  );
}
```

</details>
## Description

This PR updates types to accept `SharedValues` into config objects.

## Test plan

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

```tsx
import * as React from 'react';
import { Animated, Button } from 'react-native';
import {
  GestureHandlerRootView,
  MouseButton,
  NativeDetector,
  usePan,
} from 'react-native-gesture-handler';
import { useSharedValue } from 'react-native-reanimated';

export default function App() {
  const [visible, setVisible] = React.useState(true);
  const sv1 = useSharedValue(0);
  const sv2 = useSharedValue(MouseButton.LEFT);

  const gesture = usePan({
    onEnd: (e) => {
      'worklet';
      console.log('tap', e.handlerData);
    },

    activeOffsetX: [sv1, 120],
    mouseButton: sv2,
  });

  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,
              },
            ]}
          />
        </NativeDetector>
      )}
    </GestureHandlerRootView>
  );
}
```

</details>
## Description

This PR implements new component -> `LogicDetector`. It resolves the
issue of attaching gestures to inner SVG components. `LogicDetector`
communicates with a `NativeDetector` higher in the hierarchy, which will
be responsible for attaching gestures.

**Note:** attaching `Native` gestures to `LogicDetector` will be a added
in a follow up, as it is a niche feature - thus not a priority - and we
don't want to block this PR.

**Note 2:** Reanimated handlers currently only work on web for
reanimated: ^4.1

## Test plan
tested on the following code
```tsx
import React from 'react';
import { Text, View, StyleSheet } from 'react-native';
import { NativeDetector, LogicDetector, useGesture, SingleGestureName } from 'react-native-gesture-handler';

import Svg, { Circle, Rect } from 'react-native-svg';

export default function SvgExample() {
  const circleElementTap = useGesture(SingleGestureName.Tap, {
    onStart: () => {
      'worklet';
      console.log('RNGH: clicked circle')
    }
  });
  const rectElementTap = useGesture(SingleGestureName.Tap, {
    onStart: () => {
      'worklet';
      console.log('RNGH: clicked parallelogram')
    }
  });
  const containerTap = useGesture(SingleGestureName.Tap, {
    onStart: () => {
      'worklet';
      console.log('RNGH: clicked container')
    }
  });
  const vbContainerTap = useGesture(SingleGestureName.Tap, {
    onStart: () => {
      'worklet';
      console.log('RNGH: clicked viewbox container')
    }
  });
  const vbInnerContainerTap = useGesture(SingleGestureName.Tap, {
    onStart: () => {
      'worklet';
      console.log('RNGH: clicked inner viewbox container')
    }
  });
  const vbCircleTap = useGesture(SingleGestureName.Tap, {
    onStart: () => {
      'worklet';
      console.log('RNGH: clicked viewbox circle')
    }
  });

  return (
    <View>
      <View style={styles.container}>
        <Text style={styles.header}>
          Overlapping SVGs with gesture detectors
        </Text>
        <View style={{ backgroundColor: 'tomato' }}>
          <NativeDetector gesture={containerTap}>
            <Svg
              height="250"
              width="250"
              onPress={() => console.log('SVG: clicked container')}>
              <LogicDetector gesture={circleElementTap}>
                <Circle
                  cx="125"
                  cy="125"
                  r="125"
                  fill="green"
                  onPress={() => console.log('SVG: clicked circle')}
                />
              </LogicDetector>
              <LogicDetector gesture={rectElementTap}>
                <Rect
                  skewX="45"
                  width="125"
                  height="250"
                  fill="yellow"
                  onPress={() => console.log('SVG: clicked parallelogram')}
                />
              </LogicDetector>
            </Svg>
          </NativeDetector>
        </View>
        <Text>
          Tapping each color should read to a different console.log output
        </Text>
      </View>
      <View style={styles.container}>
        <Text style={styles.header}>SvgView with SvgView with ViewBox</Text>
        <View style={{ backgroundColor: 'tomato' }}>
          <NativeDetector gesture={vbContainerTap}>
            <Svg
              height="250"
              width="250"
              viewBox="-50 -50 150 150"
              onPress={() => console.log('SVG: clicked viewbox container')}>
              <LogicDetector gesture={vbInnerContainerTap}>
                <Svg
                  height="250"
                  width="250"
                  viewBox="-300 -300 600 600"
                  onPress={() =>
                    console.log('SVG: clicked inner viewbox container')
                  }>
                  <Rect
                    x="-300"
                    y="-300"
                    width="600"
                    height="600"
                    fill="yellow"
                  />
                  <LogicDetector gesture={vbCircleTap}>
                    <Circle
                      r="300"
                      fill="green"
                      onPress={() => console.log('SVG: clicked viewbox circle')}
                    />
                  </LogicDetector>
                </Svg>
              </LogicDetector>
            </Svg>
          </NativeDetector>
        </View>
        <Text>The viewBox property remaps SVG's coordinate space</Text>
      </View>
    </View >
  );
}

const styles = StyleSheet.create({
  container: {
    alignItems: 'center',
    justifyContent: 'center',
    marginBottom: 48,
  },
  header: {
    fontSize: 18,
    fontWeight: 'bold',
    margin: 10,
  },
});
```
## Description

I've noticed that typescript throws error when using composed gestures. This PR fixes this problem.

> [!NOTE]
> We decided that it is not worth to spend much time on changing those types, as the only thing that composition does is merging gestures into one object.

## Test plan

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

```tsx
import * as React from 'react';
import { SafeAreaView, Platform, View } from 'react-native';
import {
  GestureHandlerRootView,
  usePan,
  useSimultaneous,
  useTap,
  NativeDetector,
} from 'react-native-gesture-handler';

export default function App() {
  const pan = usePan({
    onUpdate: (_e) => {
      console.log('pan');
    },
  });

  const tap = useTap({});

  const g = useSimultaneous(pan, tap);

  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <SafeAreaView
        style={[{ flex: 1 }, Platform.OS === 'android' && { paddingTop: 50 }]}>
        <NativeDetector gesture={g}>
          <View />
        </NativeDetector>
      </SafeAreaView>
    </GestureHandlerRootView>
  );
}
```

</details>
## Description

This PR adds logic that filters `config` object before passing it to native side. 

## Test plan

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

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

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

  const gesture = useTap({
    onEnd: (e) => {
      'worklet';
      console.log('tap', e.handlerData);
    },

    numberOfTaps: 2,
  });

  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,
              },
            ]}
          />
        </NativeDetector>
      )}
    </GestureHandlerRootView>
  );
}
```

</details>
> [!NOTE]
> Supersede #3230

# Description

This PR contains cherry-picked commits from #3230. I've decided to
recreate this PR step-by-step instead of fixing conflicts after
migration to monorepo (though if you prefer merge/rebase into the former
let me know).

# Original description

## Description

removes the following components:
- `DrawerLayout` 
- `Swipeable`
- `BetterHorizontalDrawer` draft component
- `Swipeable` draft component

These components were also removed from all files referencing them, 
such as examples, index files, and the `App.tsx` of the common example
app

## Test plan

- see how these components are no longer accessible
- see how there are no errors, and no new warnings thrown as compared to
the `main` branch

---------

Co-authored-by: Ignacy Łątka <latkaignacy@gmail.com>
## Description

This PR removes `handleGestureEvent` from
`onGestureHandlerAnimatedEvent`. This function was trying to call
`Animated.Event`, which is not possible since it is an object. This
takes out part of `LogicDetector` logic, however it didn't work anyway
and at least standard `NativeDetector` works as it should.

> [!NOTE]
> It was previously a part of #3745

## Test plan

Tested on current `basic-example`
## Description

This PR fixes issue where using `Animated.Event` without `worklet`
callbacks was throwing error. This was because current implementation of
`hasWorkletEventHandlers` was checking all functions present in
`config`, including `changeEventCalculator` (which is in fact
`worklet`).

> [!NOTE]
> It previously contained #3748

## Test plan

Tested on the current `basic-example` app
…#3727)

## Description

I've noticed that `onPointerMove` callback is triggered on each gesture,
even if correct button is not pressed. Though events are not send, it
still means that handlers do unnecessary work under the hood. This PR
changes `PointerEventManager` so that it doesn't trigger `onPointerMove`
callback without active pointer on gestures other than `Hover`

## Test plan

Tested on example app with `console.log` added into `onPointerMove` in
`PanGestureHandler`
## Description

I've decided to split `utils` into multiple files for better readability
😅

## Test plan

`yarn ts-check`
## Description

This PR adds `runOnJS` to gesture config.

## Test plan

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

```tsx
import * as React from 'react';
import { Animated, Button, StyleSheet, View, Text } from 'react-native';
import {
  GestureHandlerRootView,
  NativeDetector,
  usePan,
  useSimultaneous,
} from 'react-native-gesture-handler';
import { useSharedValue } from 'react-native-reanimated';

const runtimeKind = (_worklet: boolean) => {
  'worklet';
  return _worklet ? 'worklet' : 'JS';
};

function SingleExampleWithState() {
  const [js, setJs] = React.useState(false);

  const gesture = usePan({
    onUpdate: () => {
      'worklet';
      console.log(
        `[SingleExampleWithState] I run on a ${runtimeKind(globalThis._WORKLET)} runtime!`
      );
    },

    runOnJS: js,
  });

  return (
    <View style={[styles.container, styles.center]}>
      <Button
        title="Change runtime"
        onPress={() => {
          setJs((v) => !v);
        }}
      />
      <NativeDetector gesture={gesture}>
        <Animated.View style={[styles.box]} />
      </NativeDetector>
    </View>
  );
}

function SingleExampleWithSharedValue() {
  const js = useSharedValue(false);

  const gesture = usePan({
    onUpdate: () => {
      'worklet';
      console.log(
        `[SingleExampleWithSharedValue] I run on a ${runtimeKind(globalThis._WORKLET)} runtime!`
      );
    },

    runOnJS: js,
  });

  return (
    <View style={[styles.container, styles.center]}>
      <Button
        title="Change runtime"
        onPress={() => {
          js.value = !js.value;
        }}
      />
      <NativeDetector gesture={gesture}>
        <Animated.View style={[styles.box]} />
      </NativeDetector>
    </View>
  );
}

function ComposedExample() {
  const [pan1JS, setPan1JS] = React.useState(false);
  const pan2JS = useSharedValue(false);

  const pan1 = usePan({
    onUpdate: () => {
      'worklet';
      console.log(
        `[ComposedExample | Pan1] I run on a ${runtimeKind(globalThis._WORKLET)} runtime!`
      );
    },

    runOnJS: pan1JS,
  });

  const pan2 = usePan({
    onUpdate: () => {
      'worklet';
      console.log(
        `[ComposedExample | Pan2] I run on a ${runtimeKind(globalThis._WORKLET)} runtime!`
      );
    },

    runOnJS: pan2JS,
  });

  const gesture = useSimultaneous(pan1, pan2);

  return (
    <View style={[styles.container, styles.center]}>
      <View style={[styles.center, { flexDirection: 'row', gap: 10 }]}>
        <Button
          title="Change Pan1 runtime"
          onPress={() => {
            setPan1JS((v) => !v);
          }}
        />

        <Button
          title="Change Pan2 runtime"
          onPress={() => {
            pan2JS.value = !pan2JS.value;
          }}
        />
      </View>

      <NativeDetector gesture={gesture}>
        <Animated.View style={[styles.box]} />
      </NativeDetector>
    </View>
  );
}

export default function App() {
  return (
    <GestureHandlerRootView style={[{ flex: 1 }, styles.center]}>
      <SingleExampleWithState />
      <SingleExampleWithSharedValue />
      <ComposedExample />
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  container: { gap: 10 },
  center: {
    display: 'flex',
    justifyContent: 'space-around',
    alignItems: 'center',
  },
  box: {
    width: 150,
    height: 150,
    borderRadius: 20,
    backgroundColor: 'pink',
  },
});
```

</details>
## Description

While writing examples I noticed that some types would be useful. This
includes types for each gesture and their events. This PR adds those
types.

**Important**: The v3 gesture types take over the names of old v2 types,
the v2 types are getting a `Legacy` prefix. This change will have to be
added to the migration guide. Added this as a
[task](https://github.com/orgs/software-mansion/projects/31/views/1?pane=issue&itemId=132253302)
to roadmap.

## Test plan

`yarn lint-js`
## Description

This PR splits `types.ts` file into multiple files to improve
readability.

## Test plan

1. `yarn ts-check`
2. `yarn lint-js`
3. `yarn circular-dependency-check`
## Description

This PR fixes incorrect implementation of `hasWorkletEventHandlers`
function. Namely:

1. Changes `in` to `has`, as `in` cannot be used to check if `Set`
contains given element;
2. Correctly wraps `some` parameter into `[]` - earlier `value` was
actually an index.

This problems resulted in callbacks being run on `JS`, even when they
should be run on `UI`.

## Test plan

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

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

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

  const gesture = usePan({
    onUpdate: () => {
      'worklet';
      console.log(getRuntimeKind());
    },
  });

  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,
              },
            ]}
          />
        </NativeDetector>
      )}
    </GestureHandlerRootView>
  );
}
```

</details>
## Description

Right now, if one changes `enabled` property of handlers to `false`,
they can never activate again. In the following example:

```ts
const [enabled, setEnabled] = useState(false);
const pan = Gesture.Pan().enabled(enabled)
```

gesture will never become active, even if `enabled` changes to `true`.
Same happens when `enabled` is set to `true` and then changed to `false`
and `true` again.

This happens because we set `enabled` to `true` by default in
`setGestureConfig`, thus the following check:

```ts
if (config.enabled !== undefined && this.enabled !== config.enabled) { ... }
```
 passes only when `config.enabled` is `false`.

I've set `enabled` to `null` by default, and then if `config.enabled` is
defined, it is assigned given value. Else default will be `true`. To fix
the issue, I've removed reset of `enabled` in `resetConfig` method.

Alternative approach would be to follow Android implementation and
always react to changes in `enabled` even in cases like `false`
$\rightarrow$ `false` and `true` $\rightarrow$ `true`.

## Test plan

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

```tsx
import React from 'react';
import { Button, StyleSheet, View } from 'react-native';
import {
  Gesture,
  GestureDetector,
  GestureHandlerRootView,
  NativeDetector,
  usePan,
} from 'react-native-gesture-handler';
import Animated, {
  useAnimatedStyle,
  useSharedValue,
} from 'react-native-reanimated';

export default function EmptyExample() {
  const [showDetector, setShowDetector] = React.useState(true);

  const [enablePan1, setEnablePan1] = React.useState(true);
  const enablePan2 = useSharedValue(true);

  const pan1 = Gesture.Pan().enabled(enablePan1);

  const pan2 = usePan({
    enabled: enablePan2,
  });

  const as = useAnimatedStyle(() => {
    return {
      backgroundColor: enablePan2.value ? 'lightgreen' : 'crimson',
    };
  });

  return (
    <GestureHandlerRootView style={styles.container}>
      <Button
        title="Toggle old API enabled!"
        onPress={() => {
          setEnablePan1((prev) => !prev);
        }}
      />

      <GestureDetector gesture={pan1}>
        <View
          style={[
            styles.box,
            { backgroundColor: enablePan1 ? 'lightgreen' : 'crimson' },
          ]}
        />
      </GestureDetector>

      <Button
        title="Toggle new API enabled!"
        onPress={() => {
          enablePan2.value = !enablePan2.value;
        }}
      />

      <Button
        title="Toggle NativeDetetor"
        onPress={() => {
          setShowDetector((prev) => !prev);
        }}
      />

      {showDetector && (
        <NativeDetector gesture={pan2}>
          <Animated.View style={[styles.box, as]} />
        </NativeDetector>
      )}
    </GestureHandlerRootView>
  );
}

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

  box: {
    width: 200,
    height: 200,
    borderRadius: 20,
  },
});
```

</details>
## Description

Seems like I forgot to change it while copying `MouseButton` tsdoc 😅 

## Test plan

### Before 

<img width="916" height="168" alt="image" src="https://github.com/user-attachments/assets/207a340d-d0c5-4673-9854-7c6e2d5aed4a" />


### After 

<img width="920" height="165" alt="image" src="https://github.com/user-attachments/assets/5ffab693-bfc6-4b34-b27f-9c6fe5fc0d13" />
## Description

Fixes #3705
Follow-up on #3283

## Test plan
In android directory run:
`./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release`.
> [!IMPORTANT]
> No specific changes were necessary to make **Gesture Handler** compatible with **React Native 0.82**.

This PR bumps basic-example app to React Native 0.82.

- ### 0.82.0 ✅
- 🏅 0.82.0-rc.5 🏅 ✅
- 0.82.0-rc.4 ✅
- 0.82.0-rc.3 ✅
- 0.82.0-rc.2 🤯
- 0.82.0-rc.1 ✅
- 0.82.0-rc.0 ✅

Tested that `basic-example` builds and works correctly.
> [!NOTE]
> Supersede #3335

This PR contains cherry picked commits from #3335 as merging changes after migrating into monorepo became quite painful 😞

1. `appProject` seems to be left unused after #3091
2. adding `configureEach` allows for [lazy task initialization](https://docs.gradle.org/current/userguide/task_configuration_avoidance.html#sec:old_vs_new_configuration_api_overview)

This PR optimises pre-build time of Gesture Handler android, taking it down from ~7s to ~0.6s, a 11x improvement.

- baseline:
  - time 6.776s ([link](https://gradle.com/s/h5iselfgzhafi))
  - time 7.391s ([link](https://scans.gradle.com/s/6sz4ssuepvema))
- optimised:
  - time 0.687s ([link](https://gradle.com/s/zwiujfjblp64s))
  - time 0.560s ([link](https://gradle.com/s/i5xqega7pazja))
  - time 0.596s ([link](https://gradle.com/s/ohcnspquhmau4))

fixes: #2865

Optimisation of the `example` app pre-build time will be done in a separate PR,
so far i took it down from 13s to 0.1s (caching & on-demand building).

- Run `./gradlew help --scan` to see pre-build performance
- Build app to see everything still works
## Description

<!--
Description and motivation for this PR.

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

This fixes an issue where the manual activation of a handler wouldn't
work.
I unfortunately didn't had time to make a full reproduction. The
scenario roughly looks like this:

```ts
const gesture = useMemo(() -> Gesture.Pan() // Note happens also without useMemo
	.manualActivation(true)
	.onTouchesMove((event, manager) => {
			// Some criteria to met:
			manager.activate()
	})
	.onUpdate(() => {
		// Wouldn't get called on a remount
	})

return <GestureDetector gesture={gesture}> // ...
```

This is being used in a component on a screen. When we navigate from the
screen the component gets unmounted and I can see that his logic is
being triggered:


https://github.com/software-mansion/react-native-gesture-handler/blob/1e1f4e035ea6d4c38684c4abc43d91dbf5b46dc6/packages/react-native-gesture-handler/src/handlers/gestures/GestureDetector/index.tsx#L152-L168

```
attachHandler 1
dropHandler 1
```

I think the screen might get frozen instead of unmounted, but not
entirely sure, but the cleanup function gets called and the handler gets
dropped.
Now, when reopening the screen the handlers will reattach, but have a
different handler tag now.

```
attachHandler 1
dropHandler 1
attachHandler 2 
```

When I now run the gesture I can see that the `manager` still has the
**previous handler tag**, and thus on the native side it can't find the
handler to activate. Its calling `setGestureHandlerState(1 /* previous
id */)` here (when I call `manager.activate`):


https://github.com/software-mansion/react-native-gesture-handler/blob/1e1f4e035ea6d4c38684c4abc43d91dbf5b46dc6/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/react/RNGestureHandlerModule.kt#L108

I found that attaching the handler tag to the GestureStateMangaer and
recreating it if it has changed fixes the issue.
If you need a full reproduction to proceed with this change I can
understand, just let me know then I can try to find time to create one 👍


## Test plan

<!--
Describe how did you test this change here.
-->

Well, shamefully I didn't create a reproduction so I guess the test plan
is "trust me bro" 😅 not the best I know - sorry.

---------

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

On android unction `setPressed` grabs the delagate without any checks.
As a result, in specific cases - such as in #3735 - pressed is
registered when it should not be.

## Test plan
The same as in #3735, tested on the following code.
<details>

```
import { useEffect, useState } from "react";
import { StyleSheet, Text, View } from "react-native";
import {
  GestureHandlerRootView,
  Pressable,
} from "react-native-gesture-handler";
import {
  KeyboardProvider,
  KeyboardStickyView,
} from "react-native-keyboard-controller";
import {
  SafeAreaProvider,
  useSafeAreaInsets,
} from "react-native-safe-area-context";

const rippleConfig = {
  color: "#666666",
  borderless: true,
  foreground: true,
};

function App() {
  return (
    <SafeAreaProvider>
      <GestureHandlerRootView>
        <KeyboardProvider
          statusBarTranslucent={true}
          navigationBarTranslucent={true}
          preserveEdgeToEdge={true}
        >
          <AppContent />
        </KeyboardProvider>
      </GestureHandlerRootView>
    </SafeAreaProvider>
  );
}

function Snackbar({
  visible,
  setSnackbarVisible,
}: {
  visible?: boolean;
  setSnackbarVisible: (visible: boolean) => void;
}) {
  const safeAreaInsets = useSafeAreaInsets();

  useEffect(() => {
    const timeout = setTimeout(() => {
      setSnackbarVisible(false);
    }, 3000);

    return () => clearTimeout(timeout);
  }, [visible, setSnackbarVisible]);

  return (
    <KeyboardStickyView offset={{ closed: -safeAreaInsets.bottom, opened: 0 }}>
      {visible && (
        <View style={[styles.snackbar]}>
          <Text style={styles.snackbarText}>Snackbar</Text>
        </View>
      )}
    </KeyboardStickyView>
  );
}

function Screen({
  setSnackbarVisible,
}: {
  setSnackbarVisible: (visible: boolean) => void;
}) {
  const safeAreaInsets = useSafeAreaInsets();
  const [count, setCount] = useState(0);

  const incrementCount = () => {
    setCount(count + 1);
  };

  const handleButtonPress = () => {
    setSnackbarVisible(true);
  };

  return (
    <View
      style={[
        styles.container,
        {
          flex: 1,
          paddingTop: safeAreaInsets.top,
          paddingBottom: safeAreaInsets.bottom,
        },
      ]}
    >
      <View style={styles.centerContent}>
        <Text style={styles.countText}>Count: {count}</Text>
        <Pressable
          style={styles.button}
          onPress={incrementCount}
          android_ripple={rippleConfig}
        >
          <Text style={styles.buttonText}>Increment</Text>
        </Pressable>
      </View>

      <View style={styles.bottomContent}>
        <Pressable
          style={styles.button}
          onPress={handleButtonPress}
          android_ripple={rippleConfig}
        >
          <Text style={styles.buttonText}>Show snackbar</Text>
        </Pressable>
      </View>
    </View>
  );
}

function AppContent() {
  const [snackbarVisible, setSnackbarVisible] = useState(false);

  return (
    <>
      <Screen setSnackbarVisible={setSnackbarVisible} />
      <Snackbar
        visible={snackbarVisible}
        setSnackbarVisible={setSnackbarVisible}
      />
    </>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#f5f5f5",
  },
  centerContent: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
    paddingHorizontal: 20,
  },
  bottomContent: {
    paddingHorizontal: 20,
    paddingBottom: 20,
    flexDirection: "row",
    justifyContent: "center",
  },
  countText: {
    fontSize: 24,
    fontWeight: "bold",
    marginBottom: 20,
    color: "#333",
  },
  button: {
    backgroundColor: "#007AFF",
    paddingHorizontal: 30,
    paddingVertical: 15,
    borderRadius: 8,
    alignItems: "center",
    justifyContent: "center",
    shadowColor: "#000",
    shadowOffset: {
      width: 0,
      height: 2,
    },
    shadowOpacity: 0.25,
    shadowRadius: 3.84,
    elevation: 5,
  },
  buttonText: {
    color: "white",
    fontSize: 20,
    fontWeight: "600",
  },
  disabledButton: {
    backgroundColor: "#CCCCCC",
    shadowOpacity: 0.1,
    elevation: 2,
  },
  disabledButtonText: {
    color: "#666666",
  },
  snackbar: {
    position: "absolute",
    left: 15,
    right: 15,
    bottom: 30,
    height: 50,
    backgroundColor: "#008000",
    paddingHorizontal: 15,
    paddingVertical: 12,
    borderRadius: 8,
    alignItems: "center",
    justifyContent: "center",
    shadowColor: "#000",
    shadowOffset: {
      width: 0,
      height: 2,
    },
    shadowOpacity: 0.25,
    shadowRadius: 3.84,
    elevation: 8,
  },
  snackbarText: {
    color: "white",
    fontSize: 14,
  },
});

export default App;
```

</details>

* install `react-native-keyboard-controller`
* Press on "Increment" to see that the button/pressable works.
* Press on "Show snackbar" to show the snackbar.
* While the snackbar is visible, press the "Show snackbar" button again.
* When the snackbar is gone, observe that the ripple effect on the "Show
snackbar" button is still visible. Don't touch this button.
* Press on "Increment" and observe that it's not working anymore.
`node` versions in our workflows were quite outdated, this PR bumps them
from `18` to `24`

Check CIs
## Description

There's a change in gesture recognizers introduced in `iOS` 26. Now when
`reset` method is called, recognizers go back to
`UIGestureRecognizerStatePossible` state. This breaks our current
behavior, because this state is mapped into
`RNGestureHandlerStateBegan`, so if for example `Pan` fails, it tries to
send event with `Began` state.

Unfortunately, changing recognizer state is not possible outside of
`touches*` methods, therefore we had to move `triggerAction` into those
callbacks.

Let me know if you see a different approach into this problem. 

Fixes #3733 

> [!WARNING]
> `triggerAction` call was already present in `Tap` right before `reset`
([see
here](https://github.com/software-mansion/react-native-gesture-handler/blob/21c4943d5769d3fff60f7bf0550c5810f6011e13/packages/react-native-gesture-handler/apple/Handlers/RNTapHandler.m#L120)).
Looks like it was called twice for some reason (but I believe that
[check for
_lastState](https://github.com/software-mansion/react-native-gesture-handler/blob/21c4943d5769d3fff60f7bf0550c5810f6011e13/packages/react-native-gesture-handler/apple/RNGestureHandler.mm#L311)
prevented any problems with this redundancy). For now I have not
included second call. If you think it is required, let me know.

## Test plan

Tested on the code provided below, on the following platforms: 

- [x] iOS 26.0 (iPhone 17 Pro)
- [x] iOS 18.5 (iPhone 16e)
- [x] OSX (macOS 15.6.1)  

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

```tsx
import { StyleSheet, View, Text } from 'react-native';

import {
  GestureHandlerRootView,
  Gesture,
  GestureDetector,
  GestureType,
} from 'react-native-gesture-handler';

function TestBox({
  gestureType,
  bgColor,
}: {
  gestureType: GestureType;
  bgColor: string;
}) {
  const handlerName = gestureType.handlerName;

  const gesture = gestureType
    .onEnd(() => {
      console.log(`[${handlerName}] onEnd`);
    })
    .onFinalize(() => {
      console.log(`[${handlerName}] onFinalize`);
    })
    .runOnJS(true);

  return (
    <View style={styles.center}>
      <Text>{handlerName}</Text>
      <GestureDetector gesture={gesture}>
        <View style={[styles.box, { backgroundColor: bgColor }]} />
      </GestureDetector>
    </View>
  );
}

export default function App() {
  return (
    <GestureHandlerRootView style={[{ flex: 1, padding: 50 }, styles.center]}>
      <TestBox gestureType={Gesture.Pan()} bgColor="#b58df1" />
      <TestBox gestureType={Gesture.LongPress()} bgColor="#f1a85d" />
      <TestBox gestureType={Gesture.Fling()} bgColor="#5df1a8" />
      <TestBox gestureType={Gesture.Tap()} bgColor="#5d8ef1" />
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  center: {
    display: 'flex',
    justifyContent: 'space-around',
    alignItems: 'center',
  },
  box: {
    height: 100,
    width: 100,
    backgroundColor: '#b58df1',
    borderRadius: 20,
    marginBottom: 30,
  },
});
```

</details>
m-bert and others added 20 commits March 24, 2026 12:25
## Description

In `Pan` gesture, offset properties are handled differently than other
props. Before they're sent to native side, they are mapped to separate
`start` and `end` offset properties. This happens when offset props are
present. However, if one explicitly sets value of offset property to
`undefined`, e.g.:

```ts
activeOffsetY: Platform.OS === 'android' ? [-75, 75] : undefined,
```

the following warning can be observed:

```
WARN  [react-native-gesture-handler] activeOffsetY is not a valid property for PanGestureHandler and will be ignored.
```

This PR adds offset properties to our filter. With this change, warning
no longer appears.

Fixes #4021

> [!NOTE]
> It could also be solved by dynamically deleting `undefined` fields
from config, but simply ignoring them sounds safer.

## Test plan

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

```tsx
import React from 'react';
import { StyleSheet, Text, View, Platform } from 'react-native';
import { usePanGesture } from 'react-native-gesture-handler';

export default function EmptyExample() {
  const panGesture = usePanGesture({
    activeOffsetY: Platform.OS === 'android' ? [-75, 75] : undefined,
  });

  return (
    <View style={styles.container}>
      <Text style={{ fontSize: 64, opacity: 0.25 }}>😞</Text>
      <Text style={{ fontSize: 24, opacity: 0.25 }}>It's so empty here</Text>
    </View>
  );
}

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

</details>
## Description

Current logic dedicated to handling `RNRootViewGestureRecognizer` on iOS
looks only for RN's native roots. When gestures are used inside Screens'
`FullWindowOverlay`, the logic doesn't do anything, but the overlay has
its own
[`RCTSurfaceTouchHandler`](https://github.com/software-mansion/react-native-screens/blob/be64b6d9a17c3a4647806f252e075b96b9f690cc/ios/RNSFullWindowOverlay.mm#L158).

This PR updates the traversal logic so it handles
`RNSFullWindowOverlayContainer` the same way as `RCTSurfaceView`.

## Test plan

I wasn't able to reproduce the problem, but in theory, it's possible
that without this change, recognizers from RNGH and
RCTSurfaceTouchHandler could run simultaneously, since this path never
ran:
https://github.com/software-mansion/react-native-gesture-handler/blob/5587435679eabe3f8690f077ba7c2ecc3e354a14/packages/react-native-gesture-handler/apple/RNRootViewGestureRecognizer.m#L55-L63
## Description

This PR bumps Reanimated version, along with worklets. Since in
`common-app` we still had Reanimated 3.18, I've also introduced
necessary changes to types.

Also, it turned out that our current setup cannot correctly resolve
dependencies - I've updated `metro.config.js` to also block
`common-app`, since this "app" only contains sources for examples and
their `node_modules` shouldn't be used.

## Test plan

Build and run example apps.
…rictly on hitslop instead (#4038)

## Description

Apple enforces a stupidly large retention offset in the default
UIControl touch tracking loop, which was conflicting with
`shouldCancelWhenOutside` and `hitSlop` props. This PR overrides the
default tracking loop to rely only on our props instead.

## Test plan

Tested on the example added in
#4018
## Description

`Native` gesture is specific and its behavior differs across platforms.
This leads to strange workarounds in our codebase (e.g.
[buttons](https://github.com/software-mansion/react-native-gesture-handler/blob/4a7639d8c83d5d467403cac702ee14ca0a479c4e/packages/react-native-gesture-handler/src/components/GestureButtons.tsx#L71)).

In this PR unifies buttons behavior by changing Native gesture.

## Test plan

Tested on expo-example app (buttons / Pressable)
## Description

This PR introduces new `Clickable` component, which is meant to be a
replacement for buttons.

> [!NOTE]
> Docs for `Clickable` will be added in #4022, as I don't want to
release them right away after merging this PR.

### `borderless`

For now, `borderless` doesn't work. I've tested clickable with some
changes that allow `borderless` ripple to be visible, however we don't
want to introduce them here because it would break other things. Also it
should be generl fix, not in the PR with new component.

## Stress test

Render list with 2000 buttons 25 times (50ms delay between renders),
drop 3 best and 3 worst results. Then calculate average.

Stress test example is available in this PR. 

#### Android

|           | $t_{Button}$ | $t_{Clickable}$ | $\Delta{t}$  |
|------------------|--------------|-----------------|--------------|
| `BaseButton`       | 1196,18      | 1292,3          | 96,12        |
| `RectButton`       | 1672,6       | 1275,68         | -396,92      |
| `BorderlessButton` | 1451,34      | 1290,74         | -160,6       |

#### iOS

|               | $t_{Button}$ | $t_{Clickable}$ | $\Delta{t}$  |
|------------------|--------------|-----------------|--------------|
| `BaseButton`       | 1101,37      | 1154,6          | 53,23        |
| `RectButton`      | 1528,07      | 1160,07         | -368         |
| `BorderlessButton` | 1330,24      | 1172,69         | -157,55      |


#### Web

|            | $t_{Button}$ | $t_{Clickable}$ | $\Delta{t}$  |
|------------------|--------------|-----------------|--------------|
| `BaseButton`       | 64,18        | 95,57           | 31,39        |
| `RectButton`       | 104,58       | 97,95           | -6,63        |
| `BorderlessButton` | 81,11        | 98,64           | 17,53        |

## Test plan

New examples

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
## Summary

The `+load` method in `RNGestureHandlerButtonComponentView` is
unconditional, but its parent `RCTViewComponentView` correctly guards
its `+load` behind `#ifdef RCT_DYNAMIC_FRAMEWORKS`.

Without the guard, the child's `+load` runs even when dynamic frameworks
are not used, causing conflicts with third-party SDKs (e.g. Akamai BMP)
that also use `+load` — particularly on x86_64 simulators where the load
order differs from arm64.

This matches the pattern used by React Native core in
`RCTViewComponentView.mm` (see facebook/react-native#37274).

## Test plan

- Build xcframework with static linking (no `USE_FRAMEWORKS=dynamic`)
- Verify `RNGestureHandlerButtonComponentView +load` is not in the
binary (`nm` check)
- Run on x86_64 simulator alongside Akamai BMP — no crash at launch
- Run on arm64 simulator — gesture handler works normally
…#3964)

On New Architecture, when a parent view has display: 'none' and siblings
change while hidden, the native UIView backing a component may be
recycled and replaced. The React view tag stays the same but the UIView
instance changes, causing gesture recognizers to be lost.

This adds a reattachHandlersIfNeeded check in flushOperations that
re-binds handlers whose native view has changed. The check is a fast
no-op (pointer comparison) when views haven't been recycled.

Fixes #3937

Tested with both v2 (Gesture.Tap()) and v3 (useTapGesture) APIs on iOS
simulator (New Architecture).

Minimal repro to verify the fix:

```ts
import React, { useCallback, useState } from 'react';
import { Button, StyleSheet, Text, View } from 'react-native';
import {
  Gesture,
  GestureDetector,
  useTapGesture,
} from 'react-native-gesture-handler';

export default function DisplayNone() {
  const [tapCountV2, setTapCountV2] = useState(0);
  const [tapCountV3, setTapCountV3] = useState(0);

  const tapV2 = Gesture.Tap()
    .runOnJS(true)
    .onStart(() => {
      setTapCountV2((c) => c + 1);
    });

  const tapV3 = useTapGesture({
    runOnJS: true,
    onActivate: () => {
      setTapCountV3((c) => c + 1);
    },
  });

  const [visible, setVisible] = useState(true);
  const [randomViews, setRandomViews] = useState(100);

  const runTest = useCallback(() => {
    setVisible(false);
    setTimeout(() => {
      setRandomViews(Math.random() * 100);
      setTimeout(() => {
        setVisible(true);
      }, 500);
    }, 500);
  }, []);

  return (
    <View style={styles.container}>
      <Text
        style={[
          styles.status,
          { color: tapCountV2 > 0 ? 'green' : 'red', fontSize: 24 },
        ]}>
        v2 Tap count: {tapCountV2}
      </Text>
      <Text
        style={[
          styles.status,
          { color: tapCountV3 > 0 ? 'green' : 'red', fontSize: 24 },
        ]}>
        v3 Tap count: {tapCountV3}
      </Text>
      <Button title="Run Test (Hide→Change→Show)" onPress={runTest} />
      <View
        style={[
          styles.wrapper,
          {
            display: visible ? 'flex' : 'none',
          },
        ]}>
        <View style={styles.row}>
          {Array.from({ length: randomViews }).map((_, i) => (
            <View key={i} style={styles.dot} />
          ))}
        </View>
        <View style={styles.boxRow}>
          <GestureDetector gesture={tapV2}>
            <View style={styles.blueBox}>
              <Text style={styles.blueBoxText}>v2</Text>
            </View>
          </GestureDetector>
          <GestureDetector gesture={tapV3}>
            <View style={styles.greenBox}>
              <Text style={styles.blueBoxText}>v3</Text>
            </View>
          </GestureDetector>
        </View>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#ecf0f1',
    padding: 8,
  },
  status: {
    fontSize: 16,
    fontWeight: 'bold',
    marginBottom: 20,
    textAlign: 'center',
  },
  wrapper: {
    marginTop: 50,
    gap: 20,
    backgroundColor: 'yellow',
  },
  row: {
    flexDirection: 'row',
  },
  dot: {
    width: 2,
    height: 2,
    backgroundColor: 'black',
  },
  boxRow: {
    flexDirection: 'row',
    gap: 20,
  },
  blueBox: {
    height: 50,
    width: 50,
    backgroundColor: 'blue',
    justifyContent: 'center',
    alignItems: 'center',
  },
  greenBox: {
    height: 50,
    width: 50,
    backgroundColor: 'green',
    justifyContent: 'center',
    alignItems: 'center',
  },
  blueBoxText: {
    color: 'white',
    fontWeight: 'bold',
  },
});

```

Steps:

- Tap the box — count increments
- Press "Hide → Change → Show" to trigger display:none + sibling change
+ show
- Tap again — count should still increment (was broken before this fix)
- Repeat multiple cycles to confirm reliability

Co-authored-by: m-bert <63123542+m-bert@users.noreply.github.com>
`Pinch` and `Rotation` are broken because they fail on [this
line](https://github.com/software-mansion/react-native-gesture-handler/blob/0e74233db6a30dee547c3782aefc4f90f0551141/packages/react-native-gesture-handler/apple/RNGestureHandler.mm#L306).
This happens because `handleGesture:fromReset` is called automatically
via [`action`
property](https://github.com/software-mansion/react-native-gesture-handler/blob/0e74233db6a30dee547c3782aefc4f90f0551141/packages/react-native-gesture-handler/apple/Handlers/RNPinchHandler.m#L36),
but when UIKit calls it, it only passes `recognizer` as first parameter
- `fromReset` is some garbage from memory. It gets treated as truthy
value, even though it shouldn't be.

Now, `handleGesture:fromRecognizer` will not be called automatically
when recognizer resets its state, so it should be safe to assume that
default `fromReset` value is `NO`.

Tested on the **transformations** example and example code from #3855 on
both, iOS 18.5 and iOS 26.0

Co-authored-by: m-bert <63123542+m-bert@users.noreply.github.com>
This PR fixes a memory leak by breaking a retain cycle between RNGestureHandler and RNManualActivationRecognizer. The RNGestureHandler owns the _manualActivationRecognizer with a strong reference, and previously the recognizer was holding a strong reference back to the handler, creating a cycle that prevented both objects from being deallocated.


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

`Hover` gesture events contain wrong pointer type. Normally, we map
`UITouchTypePencil` to `RNGestureHandlerStylus`, but `Hover` used
unmapped `UITouchTypePencil` instead.

Because those enums map to different values (`UITouchTypePencil = 2` &
`RNGestureHandlerStylus = 1`), it caused wrong pointer type to be
returned.

Fixes #3977

## Test plan

Not yet as now I don't have access to iPad 😞
Co-authored-by: m-bert <63123542+m-bert@users.noreply.github.com>
This PR introduces 3 changes to `Text` component

- Removes `forwardedRef`
- Adds memoization

Tested on "Nested Text" example

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

<!--
Description and motivation for this PR.

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

Improves the hover gesture on iOS by distinguishing between a mouse and
a stylus, if possible.

With these changes the handler will now assume a mouse (just like
previously with the enum bug), but if `zOffset` is available (iOS 16.1)
and it is over 0.0 it will instead be recognised as styus.

~*Maybe it makes more sense to invert the logic and assume a mouse as
default, and stylus if > 0. Let me know what you think.*~

See #3977 for more context.

Fixes #3977 

## Test plan

<!--
Describe how did you test this change here.
-->
Here's a small test page that can be added to the basic-example app for
testing;
```jsx
import { useState } from 'react';
import { View, Text } from 'react-native';
import {
  GestureDetector,
  GestureHandlerRootView,
  useHoverGesture,
  useSimultaneousGestures,
  useTapGesture,
} from 'react-native-gesture-handler';
import { runOnJS } from 'react-native-worklets';

const POINTER_TYPES: Record<number, string> = {
  0: "TOUCH",
  1: "STYLUS",
  2: "MOUSE",
  3: "KEY",
  4: "OTHER",
}

export default function Hover() {
  const [hoverPointerType, setHoverPointerType] = useState(-1);
  const [tapPointerType, setTapPointerType] = useState(-1);
  const hoverGesture = useHoverGesture({
    onUpdate: (event) => {
      runOnJS(setHoverPointerType)(event.pointerType);
    },
  });
  const tapGesture = useTapGesture({
    onFinalize: (event) => {
      runOnJS(setTapPointerType)(event.pointerType);
    },
  });
  const gesture = useSimultaneousGestures(hoverGesture, tapGesture);
  return (
    <GestureHandlerRootView
      style={{
        flex: 1,
        backgroundColor: 'white',
        justifyContent: 'center',
        alignItems: 'center',
      }}>
      <Text>Hover pointer type: {POINTER_TYPES[hoverPointerType] ?? "N/A"}</Text>
      <Text>Tap pointer type: {POINTER_TYPES[tapPointerType] ?? "N/A"}</Text>
      <GestureDetector gesture={gesture}>
        <View
          style={{ width: 200, height: 200, backgroundColor: 'salmon' }}></View>
      </GestureDetector>
    </GestureHandlerRootView>
  );
}
```

Mouse is testable in the iPad simulator by enabling `I/O -> Input ->
Send Pointer to Device` in the top menus.
Mouse is also testable on a real iPad by Linking keyboard and mouse to
it from a mac in the Displays settings.
Apple Pencil is only testable with an actual pencil on a real iPad.

---------

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

When one pointer is placed on the background, handlers don't to other
pointers. Instead, they fail on [this
line](https://github.com/software-mansion/react-native-gesture-handler/blob/41fbd374a85def52eb7051f96e3d4c63e6eb0724/packages/react-native-gesture-handler/android/src/main/java/com/swmansion/gesturehandler/core/GestureHandlerOrchestrator.kt#L295).
This is because on Android events with `ACTION_MOVE` are batched and all
pointers are contained within one event. As [Android docs
say](https://developer.android.com/reference/android/view/MotionEvent#getActionIndex()),
`actionIndex` is fine to check for up and down actions. However, we use
it for move action and it always returns `0`.

To solve this problem, I've added loop that checks whether any pointer
from event is tracked by the handler.

Fixes #3995

## Test plan

Tested on example code from #3995
Co-authored-by: m-bert <63123542+m-bert@users.noreply.github.com>
Move `@types/react-test-renderer` from `devDependencies` to
`dependencies`.

The published `lib/typescript/jestUtils/jestUtils.d.ts` imports
`ReactTestInstance` from `react-test-renderer` (line 1). Since
`@types/react-test-renderer` is only in `devDependencies`, it's not
installed for consumers — causing `TS2307: Cannot find module
'react-test-renderer'` for projects with `skipLibCheck: false`.

The [TypeScript
handbook](https://www.typescriptlang.org/docs/handbook/declaration-files/publishing.html)
is explicit: when published `.d.ts` files reference `@types/*` packages,
those packages must be in `dependencies` so consumers resolve them
automatically. Several major packages have fixed this exact pattern:

-
[webpack/schema-utils#97](webpack/schema-utils#97)
— moved `@types/json-schema` to `dependencies` for the same reason
- [@mui/material#14508](mui/material-ui#14508)
— keeps `@types/react-transition-group` in `dependencies` because
published types extend it
-
[react-native-testing-library#1534](callstack/react-native-testing-library#1534)
— shipped `.d.ts` broke `skipLibCheck: false` consumers, fixed within 2
days

This repo dealt with the same class of issue in #1990 (move
`@types/react-native` to `dependencies` — closed only because RN 0.71+
bundled its own types, not because the approach was rejected) and #2259
(shipped `.d.ts` type errors).

Note: `react-test-renderer` is [deprecated in React
19](https://react.dev/warnings/react-test-renderer). As more projects
drop it from their own dependencies, this breakage will increase for
downstream consumers.

**Consumer reproduction:**

```bash
mkdir test-rngh && cd test-rngh
npm init -y
npm install react react-native react-native-gesture-handler typescript @types/react
```

`tsconfig.json`:
```json
{
  "compilerOptions": {
    "strict": true,
    "noEmit": true,
    "skipLibCheck": false,
    "moduleResolution": "bundler",
    "module": "esnext",
    "target": "esnext",
    "jsx": "react-jsx"
  },
  "include": ["*.ts"]
}
```

`repro.ts`:
```typescript
import type { fireGestureHandler } from 'react-native-gesture-handler/lib/typescript/jestUtils/jestUtils';
export type { fireGestureHandler };
```

```bash
npx tsc --noEmit

npm install @types/react-test-renderer
npx tsc --noEmit
```

**Upstream quality gates:**

```bash
yarn workspace react-native-gesture-handler ts-check  # PASS
yarn workspace react-native-gesture-handler build      # PASS — module + commonjs + typescript
```

Zero regressions.

Co-authored-by: m-bert <63123542+m-bert@users.noreply.github.com>
This PR enables TypeScript's
[`exactOptionalPropertyTypes`](https://www.typescriptlang.org/tsconfig/#exactOptionalPropertyTypes)
flag in `react-native-gesture-handler` and fixes all 60 resulting type
errors across 22 files.

`exactOptionalPropertyTypes` is a strict TypeScript flag (not included
in `strict: true`) that distinguishes between "property is missing" and
"property is explicitly `undefined`". When enabled, `prop?: T` means the
property can be omitted but cannot be set to `undefined` — the fix is
`prop?: T | undefined`.

This matters for downstream projects that enable this flag — without
this change, they get type errors when using gesture-handler components.
This is currently blocking [react-navigation from enabling the
flag](react-navigation/react-navigation#12995).

Three categories of fixes across v2 (class-based), v3 (hooks-based), and
web handler code:

1. **Type declarations** — Added `| undefined` to optional properties in
interfaces and types (`HitSlop`, `CommonGestureConfig`,
`BaseGestureHandlerProps`, `ButtonProps`, `ExternalRelations`,
`VirtualChild`, `IGestureHandler`, etc.)

2. **Class property types** — Updated class property declarations in
`GestureHandler.ts` to include `| undefined` for optional config
properties (`_testID`, `hitSlop`, `mouseButton`, `_activeCursor`, etc.)

3. **`WithSharedValueRecursive` mapped type** — Fixed the boolean
special case in `ReanimatedTypes.ts` to preserve `undefined` from
optional properties using `| Extract<T[K], undefined>`

All changes are type annotations only — zero runtime changes.

The `src/specs/` files are consumed by `@react-native/codegen`. One spec
file was modified (`RNGestureHandlerDetectorNativeComponent.ts` — 7
`DirectEventHandler` event props). Adding `| undefined` to these is safe
— the codegen parser
([`parseTopLevelType.js`](https://github.com/facebook/react-native/blob/main/packages/react-native-codegen/src/parsers/typescript/parseTopLevelType.js))
explicitly strips `undefined` from union types and treats the result
identically to `prop?: T`.

I verified that the codegen schema output is **byte-identical** between
`main` and this branch:
```bash
node node_modules/@react-native/codegen/lib/cli/combine/combine-js-to-schema-cli.js \
  /tmp/schema.json --libraryName rngesturehandler_codegen \
  packages/react-native-gesture-handler/src/specs/RNGestureHandlerDetectorNativeComponent.ts \
  packages/react-native-gesture-handler/src/specs/RNGestureHandlerButtonNativeComponent.ts
```

All quality gates pass:

- `yarn workspace react-native-gesture-handler ts-check` — 0 errors
- `yarn workspace react-native-gesture-handler lint-js` — 0 errors
- `yarn workspace react-native-gesture-handler test` — 5 suites, 41
tests pass
- `yarn workspace react-native-gesture-handler
circular-dependency-check` — pass

Co-authored-by: m-bert <63123542+m-bert@users.noreply.github.com>
`numberOfPointers` was not included in `Native` gesture events on `iOS`.
This PR fixes this problem.

I've also run `yarn format:apple` to fix formatting.

Tested on expo-example (logging events from `RectButton` on main screen)

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

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

On iOS, `onFinalize` callback is not triggered when `Tap` fails. Turns
out that `handleGesture:` is not called from `interactionsCancelled`
method. This PR fixes this problem.

## Test plan

Tested on iOS 26.1 and 18.5.

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

```tsx
import { StyleSheet, View } from 'react-native';
import {
  GestureDetector,
  GestureHandlerRootView,
  useTapGesture,
} from 'react-native-gesture-handler';

export default function App() {
  const tap = useTapGesture({
    onFinalize: () => {
      console.log('[tap] onFinalize');
    },
    onTouchesCancel: () => {
      console.log('[tap] onTouchesCancel');
    },
  });

  return (
    <GestureHandlerRootView style={styles.container}>
      <GestureDetector gesture={tap}>
        <View style={styles.box} />
      </GestureDetector>
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  box: {
    width: 100,
    height: 100,
    backgroundColor: 'crimson',
    borderRadius: 10,
  },
});
```

</details>
Co-authored-by: m-bert <63123542+m-bert@users.noreply.github.com>
## Summary

The `+load` method in `RNGestureHandlerButtonComponentView` is
unconditional, but its parent `RCTViewComponentView` correctly guards
its `+load` behind `#ifdef RCT_DYNAMIC_FRAMEWORKS`.

Without the guard, the child's `+load` runs even when dynamic frameworks
are not used, causing conflicts with third-party SDKs (e.g. Akamai BMP)
that also use `+load` — particularly on x86_64 simulators where the load
order differs from arm64.

This matches the pattern used by React Native core in
`RCTViewComponentView.mm` (see facebook/react-native#37274).

## Test plan

- Build xcframework with static linking (no `USE_FRAMEWORKS=dynamic`)
- Verify `RNGestureHandlerButtonComponentView +load` is not in the
binary (`nm` check)
- Run on x86_64 simulator alongside Akamai BMP — no crash at launch
- Run on arm64 simulator — gesture handler works normally
Co-authored-by: m-bert <63123542+m-bert@users.noreply.github.com>
Co-authored-by: m-bert <63123542+m-bert@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot April 2, 2026 12:01
@m-bert m-bert changed the base branch from main to v2-stable April 2, 2026 12:04
Copilot AI requested a review from m-bert April 2, 2026 12:07
@m-bert
Copy link
Copy Markdown
Collaborator

m-bert commented Apr 2, 2026

Seems like technology is not there yet

#4050

@m-bert m-bert closed this Apr 2, 2026
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.