# @rootnative/impulse > Declarative gesture primitives for React Native, built on > react-native-gesture-handler. One hook per intent. Hooks return shared values > and a gesture; Impulse starts no animation and ships no components. Status: 0.0.0-alpha.1. Eight intents ship. A device sweep on an Android emulator and an iOS simulator drove every intent. It proves mechanics, not feel, so every default below is still a design intention. Docs: https://rootnative.github.io/impulse/ Full: https://rootnative.github.io/impulse/llms-full.txt Source: https://github.com/rootnative/impulse ## Install npm install @rootnative/impulse Peers: react, react-native, react-native-gesture-handler, react-native-reanimated, react-native-worklets. Render a above any component using a gesture — if it is missing, gestures never fire and nothing warns. ## What ships useTap, useDoubleTap, useLongPress, useDrag, usePan, useSwipe, usePinch, useRotate — intent hooks. useGestures — composition. useRawGesture — the escape hatch. GestureDetector is re-exported from the package entry. NOT IMPLEMENTED, do not import: useHover, useEdgeSwipe. They are designed only. usePan and useDrag are not synonyms. useDrag owns a position: x and y accumulate across gestures and bounds clamps them. usePan owns nothing: it reports translation, velocity, and a per-frame change the consumer adds up, and it zeroes at every gesture. useSwipe is a pan judged at release. directions is also the activation criterion — an all-horizontal list gives a directional threshold, so the swipe shares a view with a vertical scroller with no relation declared. usePinch owns a scale that accumulates across gestures, so min and max are the viewer's zoom range, not one gesture's. focal is the midpoint between the fingers and the point the zoom must happen about — scaling about the view's centre slides the content out from under them. It has NO activation criteria: RNGH's pinch exposes no threshold, so a pinch and a pan are separated by a relation alone — useGestures([pinch, pan], { mode: 'simultaneous' }). useRotate reports DEGREES, not the radians RNGH reports. This is the one place Impulse changes a unit: min: -45, not -Math.PI / 4. It applies to angle, gestureAngle, velocity and the min/max range. The angle accumulates and does NOT wrap — a second full turn reports 720. anchor is the point the turn happens about, the counterpart of pinch's focal. It has NO activation criteria either, so rotate and pinch are composed simultaneous. Subpaths: @rootnative/impulse/{tap,double-tap,long-press,drag,pan,swipe,pinch,rotate,compose,raw,gesture-handler} ## The shape of every hook const tap = useTap({ onTap: select }) tap.gesture // hand to tap.ref // name it in another hook's alongside / blocks / deferTo tap.isActive // SharedValue useDrag adds x and y, both SharedValue, both writable, both accumulating across gestures. ## Threads — the callback name states the thread JS thread: onTap, onDoubleTap, onLongPress, onLongPressEnd, onDragStart, onDragEnd, onPanStart, onPanEnd, onSwipe, onSwipeEnd, onPinchStart, onPinchEnd, onRotateStart, onRotateEnd. Ordinary functions; Impulse owns the scheduleOnRN boundary. Every callback that reports an ending takes a second argument, { cancelled }: onTap, onDoubleTap, onLongPressEnd, onDragEnd, onPanEnd, onSwipeEnd, onPinchEnd, onRotateEnd. It fires on both endings. cancelled is true when the system took the gesture away — a competing gesture won, the app went to the background, or a relation handed the touch on. onSwipe fires for a commit only and takes no second argument. Worklets: onBegin, onUpdate, onFinalize. Mark with 'worklet'. A plain function there makes Impulse warn once in development, and the warning names the hook and the callback. Without the directive, the first touch that reaches the phase throws "Tried to synchronously call a Remote Function". Worklets are direct gesture dependencies, so keep their identity stable — an inline worklet rebuilds the gesture every render. An inline JS-thread callback does not. ## Composition useGestures(members, { mode }) where mode is race | simultaneous | exclusive. A composed result is itself a member. Precedence reads left to right. A tap and a double tap are exclusive with the double tap FIRST, never race — a single tap recognizes on first release and would win every race. The composed single tap then pays maxDelay in latency. useGestures does not accept coexistence options. Put them on the members. ## Coexistence alongside -> both recognize (simultaneousWithExternalGesture) blocks -> this gesture wins (blocksExternalGesture) deferTo -> the named gesture wins (requireExternalGestureToFail) Name another hook's .ref, not its .gesture. TRAP: a relation target must carry a handlerTag. React Native's ScrollView has none, so the relation is SILENTLY dropped on every platform. Use gesture-handler's ScrollView / FlatList, from @rootnative/impulse/gesture-handler. TRAP: a component ref needs `as unknown as GestureReference`. RNGH types the target as a ref to a component type, which is not what ref={} produces. ## Defaults useTap: pointers 1, maxDuration 500ms, maxDistance 10pt useDoubleTap: same, plus maxDelay 500ms (probably too generous; 250-300 is closer to what platforms use) useLongPress: minDuration 500ms, maxDistance 10pt useDrag: axis both, threshold 10pt, elastic 0, unbounded usePan: axis both, threshold 10pt useSwipe: directions all four, threshold 10pt, commitDistance 80pt, commitSpeed 800pt/s usePinch: initial 1, unbounded, no activation criteria useRotate: initial 0, unbounded, no activation criteria No default has a feel result. The device sweep proved that the drag threshold, commitDistance, and maxDelay at 500ms and 250ms work. commitSpeed is not tested, because neither harness can make a swipe that fast. ## isActive does not mean the same thing in every hook useTap and useDoubleTap set it at touch-down. For useTap that is a real pressed state. For useDoubleTap it is true for every ordinary single tap on the view too, so do not drive a pressed state from it. useLongPress, useDrag, usePan, useSwipe, usePinch and useRotate set it at recognition. ## Known gaps a consumer can hit - A cancel is reported, but its velocity is not a throw. The finger never lifted on a cancelled gesture, so springing on velocity when cancelled is true flings a view the user never released. - useLongPress maxDistance cancels an ACTIVE press on web, against RNGH's own documented contract. Hold-then-drag does not work there at the default. - A missing fails silently and Impulse cannot warn. - Web is surveyed by hand; no jsdom test pins any of it. ## Out of scope, by decision No components. No animation vocabulary, springs or easing. No haptics policy. useDrag returns values and no style — @rootnative/inertia-gestures has a useDrag that returns an animatedStyle and requires @rootnative/inertia.