# Gesture Handling
> Implement touch gestures with React Native Gesture Handler for swipe, pan, pinch, and long press interactions
## When to Use
- Adding swipe-to-delete or swipe-to-reveal actions
- Building draggable cards, bottom sheets, or reorderable lists
- Implementing pinch-to-zoom on images or maps
- Creating custom navigation gestures (swipe back, pull to refresh)
- Handling simultaneous and competing gesture recognition
## Instructions
1. **Use React Native Gesture Handler v2 (RNGH) with the declarative API.** The v2 API uses `Gesture` objects composed with `GestureDetector`, replacing the old component-based API.
```bash
npx expo install react-native-gesture-handler react-native-reanimated
```
Wrap your app root with `GestureHandlerRootView`:
```tsx
import { GestureHandlerRootView } from 'react-native-gesture-handler';
export default function App() {
return (
);
}
```
2. **Create a pan gesture for draggable elements.**
```tsx
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated';
function DraggableCard() {
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
const savedX = useSharedValue(0);
const savedY = useSharedValue(0);
const pan = Gesture.Pan()
.onStart(() => {
savedX.value = translateX.value;
savedY.value = translateY.value;
})
.onUpdate((event) => {
translateX.value = savedX.value + event.translationX;
translateY.value = savedY.value + event.translationY;
})
.onEnd(() => {
translateX.value = withSpring(0);
translateY.value = withSpring(0);
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: translateX.value }, { translateY: translateY.value }],
}));
return (
Drag me
);
}
```
3. **Implement swipe-to-delete with a horizontal pan gesture.**
```tsx
function SwipeableRow({ onDelete, children }: Props) {
const translateX = useSharedValue(0);
const DELETE_THRESHOLD = -100;
const pan = Gesture.Pan()
.activeOffsetX([-10, 10]) // Activate only for horizontal movement
.onUpdate((e) => {
translateX.value = Math.min(0, e.translationX); // Only swipe left
})
.onEnd(() => {
if (translateX.value < DELETE_THRESHOLD) {
translateX.value = withTiming(-300, {}, () => {
runOnJS(onDelete)();
});
} else {
translateX.value = withSpring(0);
}
});
const style = useAnimatedStyle(() => ({
transform: [{ translateX: translateX.value }],
}));
return (
{children}
);
}
```
4. **Implement pinch-to-zoom.**
```tsx
function ZoomableImage({ source }: { source: ImageSourcePropType }) {
const scale = useSharedValue(1);
const savedScale = useSharedValue(1);
const pinch = Gesture.Pinch()
.onUpdate((e) => {
scale.value = savedScale.value * e.scale;
})
.onEnd(() => {
savedScale.value = scale.value;
if (scale.value < 1) {
scale.value = withSpring(1);
savedScale.value = 1;
}
});
const style = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
return (
);
}
```
5. **Compose multiple gestures.** Use `Gesture.Simultaneous()` for gestures that should work together (pan + pinch) and `Gesture.Exclusive()` for competing gestures (tap vs. long press).
```tsx
const pan = Gesture.Pan().onUpdate(/* ... */);
const pinch = Gesture.Pinch().onUpdate(/* ... */);
// Both gestures active at the same time
const combined = Gesture.Simultaneous(pan, pinch);
// Only one gesture wins
const tap = Gesture.Tap().onEnd(handleTap);
const longPress = Gesture.LongPress().minDuration(500).onEnd(handleLongPress);
const exclusive = Gesture.Exclusive(longPress, tap); // Long press takes priority
return {/* ... */};
```
6. **Use `runOnJS` to call JavaScript functions from gesture worklets.** Gesture callbacks run on the UI thread. To call React state setters or navigation, wrap them with `runOnJS`.
```tsx
const tap = Gesture.Tap().onEnd(() => {
runOnJS(navigation.navigate)('Details');
});
```
7. **Set activation constraints to prevent accidental activation.**
```tsx
const pan = Gesture.Pan()
.minDistance(10) // Minimum pixels before activation
.activeOffsetX([-20, 20]) // Only activate for horizontal movement
.failOffsetY([-5, 5]); // Fail if vertical movement exceeds threshold
```
## Details
**RNGH v2 vs. v1:** The v2 declarative API (`Gesture.Pan()`) replaces v1's component API (``). v2 is composable, works directly with Reanimated worklets, and supports gesture composition natively.
**UI thread execution:** RNGH gesture callbacks and Reanimated worklets run on the native UI thread, not the JavaScript thread. This means gestures remain responsive even if JavaScript is busy. Always use shared values (`useSharedValue`) instead of React state for gesture-driven animations.
**Gesture states:** Each gesture transitions through states: UNDETERMINED -> BEGAN -> ACTIVE -> END (or FAILED/CANCELLED). Use `onStart` (BEGAN), `onUpdate` (ACTIVE), and `onEnd` (END) to respond at each phase.
**Common mistakes:**
- Forgetting `GestureHandlerRootView` at the app root (gestures silently fail)
- Using React state instead of shared values in gesture callbacks (drops frames)
- Not setting activation offsets (pan interferes with scroll)
- Calling JavaScript functions directly in worklets without `runOnJS`
## Source
https://docs.swmansion.com/react-native-gesture-handler/
## Process
1. Read the instructions and examples in this document.
2. Apply the patterns to your implementation, adapting to your specific context.
3. Verify your implementation against the details and edge cases listed above.
## Harness Integration
- **Type:** knowledge — this skill is a reference document, not a procedural workflow.
- **No tools or state** — consumed as context by other skills and agents.
## Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.