Skip to main content
GTK

Gesture Handler

react-native-gtkx/gesture-handler reimplements react-native-gesture-handler 3.x's semantics on top of this platform's own gesture responder system. It is not a port: nothing from RNGH's src/ is vendored, adapted or transcribed — upstream's implementation is used as a blueprint for behavior, and the behavior is rebuilt on GTK4 event controllers. Both the Metro and vite presets alias the react-native-gesture-handler package name onto this subpath, so an app that already imports from that name changes nothing in its source to run here.

For the responder system this subpath is built on — PanResponder, View's touch props, Pressable — see Gestures.

import { Gesture, GestureDetector } from "react-native-gesture-handler"

const offset = useSharedValue(0)
const start = useSharedValue(0)

const pan = Gesture.Pan()
.activeOffsetY([-10, 10])
// translationY is measured from where THIS gesture activated, so it starts
// at zero on every new grab. Capturing the view's existing position first
// is what lets the drag continue from there instead of snapping back.
.onStart(() => {
start.value = offset.value
})
.onUpdate((event) => {
offset.value = start.value + event.translationY
})

const card = (
<GestureDetector gesture={pan}>
<Animated.View style={[styles.card, animatedStyle]} />
</GestureDetector>
)

Ten recognizers exist, in two spellings each (nine of them also have a hook; see Recognizers), a GestureDetector to mount them, composition and cross-gesture relations, GestureStateManager, the enums (State, Directions, HoverEffect, MouseButton, PointerType), and the platform's own ScrollView/FlatList/TextInput/Switch/Touchables re-exported under RNGH's names. What is not implemented is grouped at the end of this page, each with the one mechanical reason it throws.

GestureHandlerRootView​

GestureHandlerRootView renders a View with style ?? { flex: 1 } — the same default upstream's three platform implementations agree on. An explicit style prop replaces the default box rather than merging with it: an app that passes style={{ height: 100 }} gets a 100px box with no flex, not a flexing one with a height added on top.

Upstream's root view has a second job — marking the subtree as gesture-arbitrating — that this platform does not need to reproduce: the responder system's lock is already global, so there is no scope for a provider to draw. GestureHandlerRootView is therefore a plain layout box, faithful to upstream's rendered output, with nothing else attached to it.

An app places one at the root of its tree, as upstream's own documentation recommends, so that anything relying on the default flex: 1 to fill the screen has it.

GestureDetector​

The gallery&#39;s Gesture detector section: Pan, Tap and LongPress recognizers configured with activateAfterLongPress, activeOffset/failOffset, hitSlop and numberOfTaps.

GestureDetector renders exactly one child and adds no widget of its own. It reaches the child's underlying GTK widget through the same ref-forwarding seam createAnimatedComponent uses, and merges its recognizer's responder props into the child's own — a child with its own onTouchStart keeps working alongside the gesture. Passing a fragment, a string, or more than one child throws, naming the requirement, because there is nothing for a second widget to attach to. Passing something that is not a gesture spec — not built with Gesture.*(), a hook, or a composer — throws as well, naming the methods that do produce one.

If the child does not forward a ref to a widget-backed component at all — an opaque wrapper that renders, say, an Animated.View internally without forwarding its own ref or unknown props onto it — GestureDetector falls back to a context-based attachment instead: one of this platform's own components mounted somewhere inside that child can claim the gesture on its own widget. This exists because react-native-sortables' v3 gesture-handler integration hands GestureDetector exactly such a wrapper.

hitSlop, shouldCancelWhenOutside, and the x/y fields on every payload are all measured against the gesture's own view — which is why the widget still matters even though no event travels through it directly.

userSelect, touchAction and enableContextMenu are accepted and ignored: they are Web-only upstream (no text selection to suppress, no CSS touch-action, no context-menu default to cancel on this platform), and accepting them keeps source that targets several platforms portable.

A native ancestor further up the widget tree that steals the interaction mid-drag — a ScrollView above a GestureDetector, for instance — reports as a cancellation to every recognizer built on it: onEnd/onFinalize fire with success: false, not a clean ending. The responder system tells a theft (GTK denies the claim) apart from an ordinary release by watching for the ->DENIED transition on the GTK sequence and routing it to the cancel path rather than the release path.

Recognizers​

All ten recognizers run on one shared state machine — UNDETERMINED → BEGAN → (ACTIVE → END) or FAILED/CANCELLED — with the difference between kinds being which predicates the machine evaluates and which callbacks are offered. Tap and LongPress, for example, are the same machine as Pan with different predicates over the same event stream and the same grant channel.

Every recognizer has two spellings: the chainable builder (Gesture.Pan(), deprecated upstream since 3.1.0 but still what most shipped consumers call) and a hook (usePanGesture(), the spelling upstream is migrating to). Both produce the same internal gesture spec; neither is a second implementation.

RecognizerBuilderHookInput it needsReports travel (onUpdate/onChange)
PanGesture.Pan()usePanGesture()pointeryes
TapGesture.Tap()useTapGesture()pointerno
LongPressGesture.LongPress()useLongPressGesture()pointerno
NativeGesture.Native()useNativeGesture()pointeryes
PinchGesture.Pinch()usePinchGesture()touchpadyes
RotationGesture.Rotation()useRotationGesture()touchpadyes
FlingGesture.Fling()useFlingGesture()pointerno
ManualGesture.Manual()useManualGesture()pointeryes
HoverGesture.Hover()useHoverGesture()pointer (no button)yes
ForceTouchGesture.ForceTouch()— (upstream has none either)stylusyes

Pinch and Rotation are driven by a touchpad rather than by the pointer, and ForceTouch is driven by a stylus — see the recognizers that need other hardware below. Every other kind runs on the ordinary pointer stream.

One pointer, not multiple touches​

There is exactly one pointer on this platform, and every payload's pointerType reads MOUSE except on ForceTouch, which reads STYLUS — the only kind whose reading is honestly not a mouse. The responder system fabricates one touch per pointer and has no virtual-touch protocol to draw a second contact point from. minPointers(2), numberOfPointers(2), and every other multi-pointer configuration are therefore honestly unreachable: those recognizers simply never activate, rather than silently behaving as if a single finger satisfied a two-finger requirement.

Common configuration and callbacks​

Every recognizer accepts:

OptionEffect
enabledTurns the recognizer on or off.
hitSlopExtra area a press still counts in, in RNGH's spelling — a plain number, or a per-edge object. Unlike a View's own hitSlop, a negative number shrinks the area, and { left: 0, width: 32 } anchors a strip to one edge.
shouldCancelWhenOutsideWhether wandering off the view cancels the gesture. Defaults differ per kind — noted in each section below.
manualActivationOnly an explicit GestureStateManager/.activate() call can activate the gesture; the ordinary predicate is not enough on its own.
withRef() / a raw handler tagNames this gesture for a relation written on another one.
withTestId() / testIDA label carried on the config for introspection.

And the callbacks:

CallbackFires
onBeginEntering BEGAN.
onStart (onActivate in the hook spelling)Entering ACTIVE.
onUpdate, onChangeOn travel — continuous kinds only; see the per-kind sections.
onEnd (onDeactivate)Leaving ACTIVE/BEGAN for END or a cancellation.
onFinalizeAlways last, whatever the outcome.
onTouchesDown, onTouchesMove, onTouchesUp, onTouchesCancelled (onTouchesCancel)Raw touch data, independent of the recognizer's own state.

The builder spelling's ending callbacks take (event, success); the hook spelling instead reads a canceled field off one event argument, and has no onChange at all — changeX/changeY are always present on the update payload. Tap, LongPress and Fling are discrete and offer no onUpdate/onChange in either spelling: a gesture with no travel to report has nothing for those callbacks to carry.

runOnJS is accepted and does nothing: it asks for the JS runtime, and there is exactly one runtime here, so every callback already runs where it is asking. averageTouches, enableTrackpadTwoFingerGesture, cancelsTouchesInView, activeCursor and mouseButton are accepted and inert — each is platform-specific upstream too (Android-only, iOS-only or Web-only respectively), and inert off its own platform there as well.

The three relation methods — simultaneousWithExternalGesture, requireExternalGestureToFail, blocksExternalGesture — are covered in Cross-gesture relations.

Pan​

OptionEffect
activeOffsetX / activeOffsetY / failOffsetX / failOffsetYA single number is directional by its sign — activeOffsetX(20) bounds only the positive side. Failure is tested with strict comparisons where activation uses non-strict ones, so a translation exactly on a bound activates.
minDistanceDefaults to 10, unless an activeOffset* or minVelocity* option is set — then distance stops applying and those are the criteria instead.
minVelocity, minVelocityX, minVelocityYVelocity thresholds, in addition to or instead of distance.
minPointers, maxPointersPointer-count bounds — see One pointer, not multiple touches.
activateAfterLongPressActivates on a timer rather than on the next pointer movement. 0 (the default) means no hold at all.

translationX/translationY are measured from the point of activation, not from the press — a fresh grab always starts at zero, which is why an app capturing a running offset does so in onStart rather than by reading the translation directly (see the example at the top of this page). velocityX/velocityY are the last inter-event delta, not a smoothed figure — see the fling deviation below, which reads the same number.

Tap​

Tap activates on the release, not on the press, so the interaction stays available to anything else watching the same pointer while a tap is still being decided — it never holds the responder until the instant it wins.

OptionEffect
numberOfTapsTaps required. Between them the gesture stays BEGAN, holding nothing; onBegin fires once for the whole sequence.
maxDurationDefaults to 500ms, re-armed on every press of a sequence. A press held past it fails on the timer, pointer still down.
maxDelayDefaults to 500ms — how long the next tap may take to arrive before the sequence gives up.
maxDistanceA radius from the press, not a per-axis limit — the tap-vs-drag rule. Has no default, matching upstream: an unconfigured tap accepts any travel that stays inside the view.
maxDeltaX, maxDeltaYPer-axis limits, independent of maxDistance.
minPointersChecked against the most pointers the interaction ever had at once. Above 1, see One pointer, not multiple touches.
shouldCancelWhenOutsideOn by default, from the constructor. A press that wanders off the view is not a tap on it.

Differs from react-native-gesture-handler: useTapGesture() defaults shouldCancelWhenOutside to true here, matching Gesture.Tap(). Upstream's own hook forgets to set this default even though its builder and its native handler config both do, so its two spellings disagree with each other; both spellings agree here.

LongPress​

LongPress activates on a timer, with the pointer standing still — waiting for the next pointer movement would mean waiting forever for a press-and-hold.

OptionEffect
minDurationDefaults to 500ms.
maxDistanceDefaults to 10, measured from the press for the whole gesture rather than re-based at activation. Travelling past it before the press matures fails the gesture; travelling past it after cancels it, so onEnd/onFinalize report false.
numberOfPointersAbove 1, see One pointer, not multiple touches.
shouldCancelWhenOutsideOn by default.
event.durationMilliseconds since the press. Upstream carries this on LongPress alone; every payload here carries it, since there is one payload type across all ten kinds.

Differs from react-native-gesture-handler: minDuration(0) activates on the next tick rather than synchronously inside the press. Nothing observable depends on the difference.

Native​

Native stands for the widget underneath the detector — the one platform-specific rule that follows from that is that it never takes the responder. Taking it is what makes this platform claim CLAIMED on the GTK sequence and suspend kinetic scrolling on every enclosing scrollable, and a gesture whose whole meaning is "the native scroller is handling this" cannot be the thing that switches the native scroller off. It reports what happens and yields.

OptionEffect
activationBEGAN on press, ACTIVE once the pointer has traveled 15px — where a native scrollable would have started scrolling. A lift before that fails rather than ends.
shouldActivateOnStartTakes the gesture on the press itself — the shape for a native view that is a button rather than a scrollable.
disallowInterruption, yieldsToContinuousGesturesRecorded on the config, for the relation registry to read; neither changes behavior by itself.
shouldCancelWhenOutsideOn by default.
the callbacksAll present; Native is continuous, so it reports onUpdate/onChange travel like Pan. They arrive from the touch props (which fire regardless of responder status) rather than from the responder move event, since Native never holds the responder.
a sequence taken away mid-dragReported as a cancellation — see the ancestor-steals-the-sequence note above.

Pinch and Rotation — the two that need a touchpad​

The gallery&#39;s Pinch and rotation section: Gesture.Pinch() and Gesture.Rotation(), both driven by a touchpad.

Both are driven by a touchpad rather than by the pointer: a pinch is not a pointer event, it is a conclusion libinput draws from two fingers moving on a device it has classified as a touchpad, delivered as zwp_pointer_gestures_v1 and turned by GDK into GDK_TOUCHPAD_PINCH. GtkGestureZoom and GtkGestureRotate read the scale and the angle directly off that event rather than reconstructing them from tracked positions — a more direct path than upstream's own ScaleGestureDetector, which tracks two real touches and has no touchpad path of its own. With no touchpad attached, neither gesture ever begins; a mouse cannot produce the input either recognizer needs.

Recognition and arbitration are otherwise unchanged: the same state machine, the same callbacks, the same relation maps, the same broadcast cancel as every other kind. Gesture.Simultaneous(pinch, rotation) behaves exactly like Gesture.Simultaneous(pan, tap); without a relation, a Pinch and a Rotation race and cancel each other like any other two gestures would.

const scale = useSharedValue(1)
const angle = useSharedValue(0)

const pinch = Gesture.Pinch().onUpdate((event) => {
scale.value = event.scale // 1 at the start, cumulative, >1 for a spread
})
const rotation = Gesture.Rotation().onUpdate((event) => {
angle.value = event.rotation // radians since the start, positive clockwise
})

const zoomable = (
<GestureDetector gesture={Gesture.Simultaneous(pinch, rotation)}>
<Animated.View style={animatedStyle} />
</GestureDetector>
)

Neither recognizer has any configuration of its own beyond what every kind shares — matching upstream, where PinchGesture and RotationGesture add zero builder methods over their common base.

FieldValue
event.scaleCumulative and multiplicative, 1 at the start of the gesture, and not re-based when it activates.
event.scaleChangeA ratio (scale composes by multiplication) — the scale itself on the first update.
event.rotationRadians since the start of the gesture, positive clockwise.
event.rotationChangeA difference in radians.
event.focalX/focalY, event.anchorX/anchorYIn the gesture view's own coordinates; absoluteX/absoluteY carry the same point in window coordinates.
event.velocityPer second — scale-per-second for Pinch, radians-per-second for Rotation. See the deviation note below.
activationRotation at 5° of accumulated rotation (upstream's own threshold). Pinch at 5% of accumulated scale change.
shouldCancelWhenOutsideOff by default — a pinch is not addressed to a point the way a tap is, so a focal point drifting off the view mid-gesture does not cancel it.
the onTouches* callbacksAccepted, and never fire — there is no touch sequence behind a touchpad gesture, matching upstream's own behavior on a trackpad.
pinch-specific / rotation-specific configNone, upstream included.

Differs from react-native-gesture-handler, in two places, both named explicitly rather than silently reproduced:

  • Velocity units. event.velocity is computed per second here, which is what upstream's own documentation promises but not what either of its web handlers actually computes: PinchGestureHandler divides by a millisecond delta and never by 1000 (a thousand times too small), and RotationGestureDetector's time delta is an addition of two timestamps rather than a subtraction, which is not a velocity at all. There is no single correct upstream number to reproduce, so the documented unit is what ships.
  • Pinch's activation threshold. Pinch activates at 5% of accumulated scale change. Upstream activates after two stages of pixel arithmetic — 30px of span change, then a further 15px — which has nothing to measure here: a touchpad pinch arrives as a ratio, with no pixel span anywhere in the chain. A percentage is the restatement, and a small one is the correct scale for it, because libinput has already decided the two fingers are pinching rather than scrolling before GTK ever sees the event — upstream's own threshold is the first such decision in its pipeline, this one is a second, smaller gate after that decision has already been made elsewhere.

Both gestures need a real touchpad and a compositor with a libinput backend to observe; the headless compositor this project's own test suite runs against has neither, so both are verified with a virtual touchpad device instead of inside that suite.

Fling​

The distinguishing fact about a fling is that it is a velocity predicate, not a distance one — a slow drag can travel exactly as far as a fast flick.

Option / ruleValue
directionA bitmask of Directions, defaulting to Directions.RIGHT. Setting two axis bits also opens the diagonal between them, with a wider cone — UP | RIGHT accepts a 45° flick that neither axis accepts alone.
the cones30° around each axis (±15°), 60° around each diagonal (±30°) — tiling the circle exactly, matching upstream.
minVelocity700 units per second, compared strictly. Not configurable, upstream included.
the deadline800ms from the press. A press that has not flung by then fails, whatever it is doing.
numberOfPointersCompared for equality against the most pointers the interaction ever had — see One pointer, not multiple touches.
when it decidesOn every move, not on release — the instant the pointer is fast enough and pointed the right way, button still down. The release is only the last chance.
the progressionBEGAN → ACTIVE → END in one synchronous step, with no onUpdate ever — a fling is discrete.

Differs from react-native-gesture-handler: velocityX/velocityY are the last inter-event delta, the same number Pan().minVelocity() reads, rather than upstream's least-squares fit over up to 20 samples inside a 300ms horizon. A fling here is more sensitive to a single long frame than upstream's smoothed figure; the deadline and the cone are unaffected.

Manual​

No configuration of its own, in either spelling — matching upstream, where ManualGesture adds zero builder methods. It begins on the press and decides nothing on its own: the GestureStateManager handed to onTouchesDown/onTouchesMove/onTouchesUp/onTouchesCancel is the whole API.

MethodTransition
.begin()UNDETERMINED → BEGAN.
.activate()BEGAN → ACTIVE, through the ordinary arbitration — a request, not a decision: it can come back parked behind requireExternalGestureToFail, or cancelled. Forced past manualActivation.
.end()BEGAN or ACTIVE → END, successfully.
.fail()BEGAN or ACTIVE → FAILED.

Differs from react-native-gesture-handler: upstream's documentation states that Manual does not end when the pointers lift. Half of that holds here — a Manual still BEGAN when the pointer comes up stays BEGAN, holding nothing. The other half does not: an ACTIVE Manual here is holding an interaction — the responder lock, the GTK sequence, suspended scrollers — and that interaction ends when the pointer does. Staying ACTIVE past it would mean holding a lock that no longer exists and never reporting an ending at all, so an ACTIVE Manual ends, successfully, with the interaction. onTouchesUp fires first and carries the state manager, for an app that wants a different ending to write it in.

Hover​

Driven by the same GTK motion controller Pressable uses for its hovered state. It goes straight to ACTIVE on the pointer crossing in, with no threshold at all, reports x/y in the gesture view's own coordinates while the pointer moves inside, and ends — not cancels — when the pointer leaves.

OptionEffect
.effect() / hoverEffectAccepted, inert — iOS's own pointer effect; upstream's web handler never branches on it either.
hitSlop, enabledAs for every other kind, including the shrinking (negative) form of hitSlop.
the callbacksHover is continuous, so onUpdate/onChange report travel and changeX/changeY carry the delta.
mouseButtonInert for this kind, matching upstream: hover never consults a button.

A hover never takes the responder — there is no press to start an interaction with, so there is no session to claim. That means a hover cannot exclude a press by itself, and mutual exclusion is still the default: a hover crossing in while a Pan on another view is still BEGAN cancels that pan, matching upstream's own behavior. Declaring simultaneousWithExternalGesture (or composing with Gesture.Simultaneous()) between a hover and anything sharing its screen avoids that, the same way upstream's own Pressable sets manualActivation on its internal hover recognizer to stop it blocking a native gesture.

ForceTouch​

Upstream does not implement ForceTouch off iOS at all, so there is no web behavior to match — the semantics below come from its documented contract.

OptionEffect
minForceDefaults to 0.2, upstream's documented default. Non-strict at the bound, like every other activation threshold here.
maxForceA ceiling that fails the gesture before activation and cancels it after — the same shape LongPress's maxDistance has. Unset means no ceiling.
feedbackOnActivationAccepted, inert — there is no haptic device on this platform.
force, forceChangeOn every payload. forceChange is a difference (the force itself on the first update).
pointerTypeAlways STYLUS for this kind — the only one that is not MOUSE, since a pressure reading can only have come from a tablet tool.

ForceTouch has no hook counterpart in either implementation — upstream's own hook tree has nine directories and no forceTouch, so Gesture.ForceTouch() is the whole API upstream offers for it, and the whole API offered here.

It is driven by GtkGestureStylus, whose pressure axis arrives already normalized to [0, 1] — upstream's documented range, so nothing is rescaled. The controller is stylus-only by default, so a mouse produces no events for it at all: that is deliberate, and it is what keeps a ForceTouch from activating at pressure 0 on a machine with no drawing tablet. Verifying the full chain end to end needs a real or virtual stylus device; the headless compositor this project's test suite runs against enumerates none.

Gesture composition​

Gesture.Race(), Gesture.Simultaneous() and Gesture.Exclusive() (and their hook equivalents useCompetingGestures(), useSimultaneousGestures() and useExclusiveGestures()) are list-builders over the three relation maps described in Cross-gesture relations, with no mechanism of their own:

  • Race adds no relation at all — racing is what happens without one.
  • Simultaneous is a pairwise fill of the simultaneous-handlers map.
  • Exclusive is a chain fill of the wait-for map, where every group waits for every group before it. A nested Exclusive inside a Simultaneous stays exclusive.

A single GestureDetector may hold a composition. It mounts every recognizer the composition contains onto the one child, and still adds no widget.

Cross-gesture relations​

The gallery&#39;s Gesture relations section: Gesture.Native() over a ScrollView, simultaneousWithExternalGesture, and requireExternalGestureToFail.

RelationMeans
requireExternalGestureToFail(other) — hook config: requireToFailThis gesture waits for other to fail.
simultaneousWithExternalGesture(other) — hook config: simultaneousWithBoth may be ACTIVE at once.
blocksExternalGesture(other) — hook config: blockother waits for this one.

A relation names the other gesture with the gesture object itself, a withRef() handle to it, or a raw handler tag. The gesture object built by either spelling is rebuilt on every render, so a relation should point at a memoized object (useMemo, a ref, or a context value) — a relation written against a stale object of a gesture that has since been rebuilt cannot be resolved. Upstream has the same constraint.

const scroll = Gesture.Pan().activeOffsetX([-10, 10]).failOffsetY([-25, 25])

const sheet = Gesture.Pan()
.activeOffsetY([-10, 10])
// Held in BEGAN — taking nothing, claiming nothing — until `scroll` fails.
.requireExternalGestureToFail(scroll)

Two locks, at two levels, deliberately not merged. The responder lock keeps its one job: one interaction belongs to React Native, one holder, one irrevocable claim on the source. Gesture arbitration is a second, JS-only registry that never talks to GTK — every relation resolves before anything is claimed. The consequences:

  • Simultaneous really means two ACTIVE gestures, each getting its own onStart/onUpdate/onEnd for the same pointer — while exactly one responder is claimed. The gesture that did not win the responder lock is driven from the touch props, which fire regardless of responder status; the holder reads the responder-move event.
  • Mutual exclusion is the default. Without a relation, the first gesture to activate cancels every other gesture watching the same interaction. A gesture that is already ACTIVE, or parked waiting for another, is cancelled by nothing except an active Gesture.Native() — which is why Native is treated as special rather than as just another recognizer.
  • END and FAILED are not the same release for a parked gesture: one waiting on another is released when that one fails or is cancelled, and cancelled when it ends — the thing it was deferring to actually happened, so its own turn never comes.

Two responder roots that nest — an island mounted inside another island's view — are one GTK widget chain, so both gestures share one interaction path and every relation behaves as it would inside a single root. Two roots that are disjoint — separate windows, or sibling islands — can never have both gestures live in one interaction at once: a relation between them is expressible and resolves to a real handler tag, it simply never has an occasion to apply, and it neither errors nor warns. requireExternalGestureToFail across disjoint roots does not deadlock for the same reason: parking only ever happens against a gesture that is live in the interaction under way, so a gesture in another root is never waited for.

GestureStateManager​

GestureStateManager.activate(handlerTag), .fail(handlerTag) and .deactivate(handlerTag) are standalone functions, keyed by a numeric handler tag rather than by a gesture object — the shape react-native-gesture-handler 3.1.0 itself exports under this name (its older .create(tag) factory survives only as a type, with no runtime value).

Each call looks the tag up in a registry populated the instant a GestureDetector mints a handler tag for a mounted recognizer, and forgotten the instant that detector unmounts, then routes to the same state-manager object Gesture.Manual()'s own onTouchesDown/onTouchesMove/onTouchesUp/ onTouchesCancel callbacks already receive — the same machinery, the same arbitration loop, nothing built twice.

MethodEffect
.activate(handlerTag)BEGAN → ACTIVE on the recognizer that tag names, through the ordinary arbitration loop.
.fail(handlerTag)BEGAN or ACTIVE → FAILED.
.deactivate(handlerTag)BEGAN or ACTIVE → END, successfully — upstream's other name for the same transition.
an unknown tagA no-op, with a development-mode warning.

Differs from react-native-gesture-handler: a tag naming no mounted recognizer — never minted, or already unmounted — does not throw. It is a no-op, warned in development rather than in production, which is not upstream's own shape (a native lookup miss) but the closest match available: loud without being fatal, matching the same no-op a gesture's own state machine already gives an out-of-order call.

react-native-sortables' own v3 gesture-handler adapter calls GestureStateManager.activate(event.handlerTag) from its own onTouchesMove, reading only the numeric tag off the event — the ordinary path for any drag using that library, not an edge case.

State, Directions and the other enums​

ExportValuesUsed for
StateUNDETERMINED 0, FAILED 1, BEGAN 2, CANCELLED 3, ACTIVE 4, END 5Every payload's state/oldState fields, so state === State.ACTIVE is ordinary, correct code.
DirectionsRIGHT 1, LEFT 2, UP 4, DOWN 8 — a bitmaskGesture.Fling().direction(). Four diagonal combinations exist by OR-ing two axis bits together (UP | RIGHT, and so on) but are not named on the public object, matching upstream.
PointerTypeTOUCH 0, STYLUS 1, MOUSE 2, KEY 3, OTHER 4Every payload's pointerType. Only MOUSE and STYLUS are ever actually reported on this platform.
HoverEffectNONE 0, LIFT 1, HIGHLIGHT 2.effect()/hoverEffect on Gesture.Hover(). Inert — iOS's own pointer effect, inert in upstream's own web handler too.
MouseButtonLEFT 1, RIGHT 2, MIDDLE 4, BUTTON_4 8, BUTTON_5 16, ALL 31 — a bitmask.mouseButton(). Inert, matching upstream off Web.

HoverEffect and MouseButton are exported as real data even though they are inert on this platform, for the same reason their knobs are accepted rather than refused: both are already accepted-and-inert configuration, and a knob that accepts a number while refusing the named constant for that number would be incoherent. Every value in all five enums is pinned against react-native-gesture-handler 3.1.0's own source numbers.

The re-exported components​

ScrollView, FlatList, TextInput, Switch, Pressable, TouchableOpacity, TouchableHighlight and TouchableWithoutFeedback are re-exported under RNGH's names as this platform's own components, unwrapped.

Upstream builds each of these with createNativeWrapper(RN.X, { disallowInterruption: true, shouldCancelWhenOutside: false }) — attaching a NativeViewGestureHandler so that its own arbitration knows about the native scrolling or the native press underneath. On this platform the responder system already is that arbitration: every one of these components already speaks it, and Gesture.Native() is how an app declares a gesture over one of them explicitly when it needs to. The wrapper has nothing to add here, so the honest re-export is the component itself.

What is not implemented​

Every export listed below throws when used — on call, on render, or on property access, naming itself — rather than silently rendering its children without gestures attached. An import this subpath does not list at all fails earlier still, at bundle time, with the bundler's own "no export named X".

The legacy handler-component API (RNGH 1.x)​

FlingGestureHandler, ForceTouchGestureHandler, LongPressGestureHandler, NativeViewGestureHandler, PanGestureHandler, PinchGestureHandler, RotationGestureHandler, TapGestureHandler and legacy_createNativeWrapper all throw, naming themselves.

These are RNGH's 1.x component API — <PanGestureHandler onGestureEvent={...}><View/></PanGestureHandler>, with its own onGestureEvent/onHandlerStateChange event shape, its own enabled/waitFor prop plumbing and its own createHandler HOC — which upstream deprecated years before it deprecated the builder spelling. The builder (Gesture.Pan() and its siblings) and the hook spelling (usePanGesture() and its siblings) are the two spellings implemented here, which is one more than upstream itself still recommends. legacy_createNativeWrapper(Component, config) attaches a NativeViewGestureHandler to an arbitrary component; it has nothing to add on this platform for the same reason the re-exported components above do not need it — the responder system is already the arbitration it would register with.

The native button family​

BaseButton, RawButton, RectButton and BorderlessButton all throw, naming themselves.

These are not RN components with a handler attached — they are RNGH's own native button views, implemented in Java and Objective-C, with an Android ripple, rippleColor/rippleRadius, borderless drawable selection, an exclusive group, and an activeOpacity applied by the native view rather than by style. No GTK widget has that set of semantics, and there is no way to fake the ripple — any implementation would be a Pressable wearing another component's name.

RefreshControl, Touchable and TouchableNativeFeedback​

All three throw, naming themselves.

TouchableNativeFeedback is Android's ripple by another name. Touchable is React Native's own deprecated mixin. RefreshControl is pull-to-refresh, which needs a scroll gesture this platform's ScrollView does not expose and a spinner widget this platform does not have.

The three new-API pieces that don't apply here​

GestureDetectorType, InterceptingGestureDetector and VirtualGestureDetector all throw, naming themselves, for three separate reasons:

  • GestureDetectorType is a type upstream, not a value. Type positions never reach this module at all — the alias is a bundler alias, so tsc resolves the real package's types from node_modules — so a runtime value under this name could only be reached by code that has already gone wrong.
  • InterceptingGestureDetector intercepts events destined for views below it. Doing that here would mean claiming a GTK sequence before deciding whether to keep it, and a claim on this platform is irrevocable — the "intercept, look, maybe give it back" shape has no GTK equivalent.
  • VirtualGestureDetector drives a gesture with no view at all. The handler-tag registry behind GestureStateManager answers "which mounted recognizer does this number mean", not "mint a recognizer with nothing to measure and no widget to attach a controller to" — every recognizer on this platform is still built by a mounted GestureDetector wrapping exactly one child.

The 2.x legacy aliases​

LegacyScrollView, LegacyFlatList, LegacyTextInput, LegacySwitch, LegacyPressable, LegacyText, LegacyRawButton, LegacyBaseButton, LegacyRectButton, LegacyBorderlessButton, LegacyRefreshControl and LegacyDrawerLayoutAndroid all throw, naming themselves.

Each is 3.x's escape hatch back to its 2.x implementation of a component whose 3.x spelling either already works here under its modern name, or is refused above with its own reason. Where the modern name works, the legacy alias would carry a promise this platform cannot keep — "this behaves like 2.x did" — since 2.x's own behavior was never implemented here to differ from. Where the modern name is refused, the alias inherits that refusal. LegacyDrawerLayoutAndroid is refused twice over: React Native itself does not ship DrawerLayoutAndroid off Android, and @react-navigation/drawer reaches for react-native-drawer-layout instead, which runs on this platform.