@rootnative/impulse 0.0.0-alpha.0 → 0.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +51 -5
  2. package/README.md +206 -11
  3. package/dist/{chunk-5BMRKYVY.js → chunk-2UZTAWUQ.js} +20 -2
  4. package/dist/chunk-BNSFDNLA.js +75 -0
  5. package/dist/chunk-DXXGWG4Q.js +136 -0
  6. package/dist/{chunk-PMR25UCT.js → chunk-HGBCIL6X.js} +1 -1
  7. package/dist/chunk-HI5PHDJY.js +94 -0
  8. package/dist/{chunk-IG5RXCYR.js → chunk-IH7SQ5X6.js} +11 -15
  9. package/dist/chunk-MWCIEVTA.js +199 -0
  10. package/dist/{chunk-F4RHM4ZK.js → chunk-NYDDZD4G.js} +58 -1
  11. package/dist/chunk-PGOQSKEJ.js +144 -0
  12. package/dist/{chunk-FR242SUF.js → chunk-TGOGIZDH.js} +13 -7
  13. package/dist/chunk-VEPUHGPN.js +12 -0
  14. package/dist/chunk-YZHAQ4XK.js +136 -0
  15. package/dist/compose/index.d.ts +1 -1
  16. package/dist/double-tap/index.d.ts +184 -0
  17. package/dist/double-tap/index.js +5 -0
  18. package/dist/drag/index.d.ts +18 -13
  19. package/dist/drag/index.js +3 -3
  20. package/dist/index.d.ts +10 -3
  21. package/dist/index.js +12 -5
  22. package/dist/long-press/index.d.ts +219 -0
  23. package/dist/long-press/index.js +4 -0
  24. package/dist/pan/index.d.ts +221 -0
  25. package/dist/pan/index.js +4 -0
  26. package/dist/pinch/index.d.ts +239 -0
  27. package/dist/pinch/index.js +4 -0
  28. package/dist/raw/index.d.ts +3 -3
  29. package/dist/raw/index.js +2 -2
  30. package/dist/rotate/index.d.ts +263 -0
  31. package/dist/rotate/index.js +4 -0
  32. package/dist/swipe/index.d.ts +254 -0
  33. package/dist/swipe/index.js +4 -0
  34. package/dist/tap/index.d.ts +34 -29
  35. package/dist/tap/index.js +4 -3
  36. package/dist/tapEvent-KSSojt_l.d.ts +28 -0
  37. package/dist/{types-Ch2HM3aP.d.ts → types-ChGKY28a.d.ts} +27 -1
  38. package/dist/{useGestureMemo-Ccv8rB0C.d.ts → useGestureMemo-BRW1EKcJ.d.ts} +1 -1
  39. package/jest-setup.cjs +32 -1
  40. package/llms.txt +155 -0
  41. package/package.json +35 -2
  42. package/src/index.ts +50 -4
  43. package/src/intents/double-tap/index.ts +6 -0
  44. package/src/intents/long-press/index.ts +6 -0
  45. package/src/intents/pan/index.ts +2 -0
  46. package/src/intents/pinch/index.ts +2 -0
  47. package/src/intents/rotate/index.ts +6 -0
  48. package/src/intents/swipe/index.ts +8 -0
  49. package/src/intents/tapEvent.ts +50 -0
  50. package/src/intents/useDoubleTap.ts +321 -0
  51. package/src/intents/useDrag.ts +49 -28
  52. package/src/intents/useLongPress.ts +391 -0
  53. package/src/intents/usePan.ts +444 -0
  54. package/src/intents/usePinch.ts +483 -0
  55. package/src/intents/useRotate.ts +507 -0
  56. package/src/intents/useSwipe.ts +585 -0
  57. package/src/intents/useTap.ts +51 -60
  58. package/src/internal/intentResult.ts +67 -0
  59. package/src/internal/phaseCallbacks.ts +51 -0
  60. package/src/internal/useGestureMemo.ts +32 -4
  61. package/src/internal/useLatestCallback.ts +2 -2
  62. package/src/raw/useRawGesture.ts +1 -1
  63. package/src/relations/index.ts +94 -0
  64. package/src/types.ts +27 -0
@@ -0,0 +1,585 @@
1
+ import { useMemo } from 'react'
2
+ import {
3
+ Gesture,
4
+ type GestureStateChangeEvent,
5
+ type GestureUpdateEvent,
6
+ type PanGesture,
7
+ type PanGestureHandlerEventPayload,
8
+ } from 'react-native-gesture-handler'
9
+ import { useSharedValue, type SharedValue } from 'react-native-reanimated'
10
+ import { scheduleOnRN } from 'react-native-worklets'
11
+ import {
12
+ useGestureMemo,
13
+ type GestureMemoOptions,
14
+ } from '../internal/useGestureMemo'
15
+ import { buildIntentResult } from '../internal/intentResult'
16
+ import { useLatestCallback } from '../internal/useLatestCallback'
17
+ import { useStableList } from '../internal/useStableList'
18
+ import { useStableRecord } from '../internal/useStableRecord'
19
+ import {
20
+ type HitSlop,
21
+ type IntentEndInfo,
22
+ type IntentResult,
23
+ type Point,
24
+ } from '../types'
25
+
26
+ /**
27
+ * How far the finger must travel before the swipe takes the touch, in points.
28
+ * The same number `useDrag` and `usePan` use.
29
+ *
30
+ * **This number is a design intention, not a measurement.** The device sweep
31
+ * of 2026-09-19 did not test it. See Known gaps in docs/docs/roadmap.md.
32
+ */
33
+ const DEFAULT_THRESHOLD = 10
34
+
35
+ /**
36
+ * How far the finger must travel for a release to commit, in points.
37
+ *
38
+ * Deliberately far above `threshold`: passing the threshold means the swipe
39
+ * is tracking, and passing this means the user meant it.
40
+ *
41
+ * **Unmeasured**, like every other default here.
42
+ */
43
+ const DEFAULT_COMMIT_DISTANCE = 80
44
+
45
+ /**
46
+ * How fast the finger must be moving for a release to commit, in points per
47
+ * second — the flick that commits without travelling `commitDistance`.
48
+ *
49
+ * **Unmeasured.**
50
+ */
51
+ const DEFAULT_COMMIT_SPEED = 800
52
+
53
+ /** Which way a swipe went. The same vocabulary as the `directions` option. */
54
+ export type SwipeDirection = 'left' | 'right' | 'up' | 'down'
55
+
56
+ /** Every direction, which is what an unset `directions` means. */
57
+ const ALL_DIRECTIONS: readonly SwipeDirection[] = [
58
+ 'left',
59
+ 'right',
60
+ 'up',
61
+ 'down',
62
+ ]
63
+
64
+ /** The intent-shaped payload a {@link useSwipe} callback receives. */
65
+ export interface SwipeEvent {
66
+ /**
67
+ * Which way the swipe went, or `null` when the release did not commit.
68
+ *
69
+ * `onSwipe` receives a payload whose direction is never `null` — that is
70
+ * the whole meaning of that callback. `onSwipeEnd` fires on every release
71
+ * of a swipe that activated, so it is the one that has to read this.
72
+ */
73
+ readonly direction: SwipeDirection | null
74
+ /**
75
+ * How far the finger travelled along the dominant axis, in points, always
76
+ * positive.
77
+ *
78
+ * Compare it against `commitDistance` to see how close a release that did
79
+ * not commit came.
80
+ */
81
+ readonly distance: number
82
+ /**
83
+ * How fast the finger was moving along the dominant axis, in points per
84
+ * second, always positive. The scalar counterpart of `velocity`, and what
85
+ * `commitSpeed` is compared against.
86
+ */
87
+ readonly speed: number
88
+ /**
89
+ * How far the finger moved since the swipe activated, signed and per axis.
90
+ *
91
+ * Measured from the activation point, so it does not include the
92
+ * `threshold` travel the finger spent before the swipe existed.
93
+ */
94
+ readonly translation: Point
95
+ /** Finger speed, in points per second, signed and per axis. */
96
+ readonly velocity: Point
97
+ /** The touch point relative to the window. */
98
+ readonly absolute: Point
99
+ /** How many fingers are down. */
100
+ readonly pointers: number
101
+ }
102
+
103
+ /**
104
+ * A {@link SwipeEvent} that committed, so its direction is known.
105
+ *
106
+ * `onSwipe` takes this rather than `SwipeEvent`, which is what saves every
107
+ * consumer of that callback a null check for a case it cannot be in.
108
+ */
109
+ export type CommittedSwipeEvent = SwipeEvent & {
110
+ readonly direction: SwipeDirection
111
+ }
112
+
113
+ /** Options for {@link useSwipe}. */
114
+ export interface UseSwipeOptions extends GestureMemoOptions {
115
+ /**
116
+ * Which directions may commit. Every direction by default.
117
+ *
118
+ * **This also sets the activation criterion**, which is the reason to
119
+ * narrow it even when the extra directions would never fire. A list that is
120
+ * entirely horizontal gives the gesture a directional threshold on the x
121
+ * axis, so a vertical scroll view under it keeps working; an entirely
122
+ * vertical list does the same on y. A mixed list has no axis to lock, so
123
+ * the threshold is radial and the swipe competes with a scroller for every
124
+ * touch.
125
+ *
126
+ * Written inline as an array is fine — the gesture is not rebuilt when the
127
+ * contents are unchanged.
128
+ */
129
+ directions?: readonly SwipeDirection[]
130
+ /**
131
+ * How far the finger must travel before the swipe activates and starts
132
+ * tracking, in points. Default `10`.
133
+ *
134
+ * This is not the commit test. It is the point at which the swipe takes the
135
+ * touch and `x` and `y` start reporting — see `commitDistance` for what
136
+ * decides that a release counts.
137
+ */
138
+ threshold?: number
139
+ /**
140
+ * How far the finger must travel for a release to commit, in points.
141
+ * Default `80`.
142
+ *
143
+ * Measured along the dominant axis, from the activation point.
144
+ */
145
+ commitDistance?: number
146
+ /**
147
+ * How fast the finger must be moving for a release to commit, in points per
148
+ * second. Default `800`.
149
+ *
150
+ * The flick: a release this fast commits even when it never travelled
151
+ * `commitDistance`. Either test is enough on its own.
152
+ */
153
+ commitSpeed?: number
154
+ /**
155
+ * Movement across the axis that makes the swipe fail, in points. Unset by
156
+ * default, which leaves RNGH's own behaviour in place.
157
+ *
158
+ * Ignored when `directions` has no single axis, which has no cross axis.
159
+ */
160
+ failOffset?: number
161
+ /**
162
+ * How many fingers must be on the view. Unset by default, which leaves
163
+ * RNGH's range of one to ten in place.
164
+ */
165
+ pointers?: number
166
+ /**
167
+ * Extra touchable area around the view, in points.
168
+ *
169
+ * Written inline as an object is fine — the gesture is not rebuilt when the
170
+ * contents are unchanged.
171
+ */
172
+ hitSlop?: HitSlop
173
+ /**
174
+ * Whether the gesture is recognized at all. Default `true`.
175
+ *
176
+ * Prefer this over unmounting the `<GestureDetector>`: a disabled gesture
177
+ * keeps its identity and its relations, so re-enabling it does not
178
+ * re-attach anything.
179
+ */
180
+ enabled?: boolean
181
+ /**
182
+ * The swipe committed. **Runs on the JS thread** — Impulse owns the
183
+ * `scheduleOnRN` boundary, so this is an ordinary function and may touch
184
+ * React state.
185
+ *
186
+ * This is the callback almost every consumer wants, and it fires only for
187
+ * the thing it is named after: a release that passed `commitDistance` or
188
+ * `commitSpeed` in a direction `directions` allows. It never fires for a
189
+ * cancelled gesture, because a swipe the system took away did not happen.
190
+ *
191
+ * `event.direction` is never `null` here.
192
+ */
193
+ onSwipe?: (event: CommittedSwipeEvent) => void
194
+ /**
195
+ * The swipe ended, committed or not. **Runs on the JS thread.**
196
+ *
197
+ * Fires on every release of a swipe that activated, which is what a view
198
+ * that followed the finger needs: `onSwipe` says to act, and this says to
199
+ * put the view back. Read `event.direction` for whether it committed —
200
+ * `null` is the release that did not.
201
+ *
202
+ * It fires for both endings, and `cancelled` says which. A cancelled
203
+ * gesture never commits, so its direction is always `null`.
204
+ */
205
+ onSwipeEnd?: (event: SwipeEvent, info: IntentEndInfo) => void
206
+ /**
207
+ * The finger went down and the gesture is now a candidate. **This is a
208
+ * worklet** — mark it with the `'worklet'` directive, and do not touch
209
+ * React state from it.
210
+ *
211
+ * The swipe has not passed `threshold` yet and may never. Undo whatever it
212
+ * sets in `onFinalize`.
213
+ */
214
+ onBegin?: (event: SwipeEvent) => void
215
+ /**
216
+ * The finger moved. **This is a worklet**, and it runs on every frame.
217
+ *
218
+ * `direction` is `null` on every one of these: a swipe is decided at
219
+ * release, not while the finger is down. Read `translation` to show the
220
+ * view following, and `distance` to show how close a commit is.
221
+ */
222
+ onUpdate?: (event: SwipeEvent) => void
223
+ /**
224
+ * The gesture is over, whether it activated or not. **This is a worklet.**
225
+ *
226
+ * `success` is `true` when the swipe activated and ended normally — which
227
+ * is not the same as committing. A release that stopped short is a
228
+ * successful gesture with no direction.
229
+ */
230
+ onFinalize?: (event: SwipeEvent, success: boolean) => void
231
+ }
232
+
233
+ /** What {@link useSwipe} returns. */
234
+ export interface UseSwipeResult extends IntentResult<PanGesture> {
235
+ /**
236
+ * How far the finger has moved along the x axis since the swipe activated.
237
+ *
238
+ * **This is movement, not a position**, on the same terms as `usePan`: it
239
+ * is zeroed at the start of every gesture and keeps its final number after
240
+ * the gesture ends, so a release animation has something to animate from.
241
+ *
242
+ * Impulse does not put it back. That is an animation, and Principle 5 keeps
243
+ * animation out of this library — `onSwipeEnd` is where a consumer springs
244
+ * it home or off the screen.
245
+ */
246
+ readonly x: SharedValue<number>
247
+ /**
248
+ * How far the finger has moved along the y axis since the swipe activated.
249
+ * Same terms as `x`.
250
+ */
251
+ readonly y: SharedValue<number>
252
+ }
253
+
254
+ /**
255
+ * Decide which direction a release commits to, or `null` for one that does
256
+ * not.
257
+ *
258
+ * The dominant axis is the one the finger travelled furthest along, and only
259
+ * that axis is tested — a release is one swipe, not two. Either test passes
260
+ * on its own: far enough, or fast enough.
261
+ *
262
+ * A dominant axis whose direction is not allowed returns `null` rather than
263
+ * falling back to the other axis. A swipe that went mostly down is not an
264
+ * left swipe, however far sideways it also drifted.
265
+ */
266
+ function pickDirection(
267
+ translationX: number,
268
+ translationY: number,
269
+ velocityX: number,
270
+ velocityY: number,
271
+ commitDistance: number,
272
+ commitSpeed: number,
273
+ allowLeft: boolean,
274
+ allowRight: boolean,
275
+ allowUp: boolean,
276
+ allowDown: boolean,
277
+ ): SwipeDirection | null {
278
+ 'worklet'
279
+ const distanceX = Math.abs(translationX)
280
+ const distanceY = Math.abs(translationY)
281
+ if (distanceX >= distanceY) {
282
+ if (distanceX < commitDistance && Math.abs(velocityX) < commitSpeed) {
283
+ return null
284
+ }
285
+ if (translationX < 0) {
286
+ return allowLeft ? 'left' : null
287
+ }
288
+ if (translationX > 0) {
289
+ return allowRight ? 'right' : null
290
+ }
291
+ return null
292
+ }
293
+ if (distanceY < commitDistance && Math.abs(velocityY) < commitSpeed) {
294
+ return null
295
+ }
296
+ if (translationY < 0) {
297
+ return allowUp ? 'up' : null
298
+ }
299
+ if (translationY > 0) {
300
+ return allowDown ? 'down' : null
301
+ }
302
+ return null
303
+ }
304
+
305
+ /**
306
+ * Recognize a directional swipe.
307
+ *
308
+ * ```tsx
309
+ * const swipe = useSwipe({
310
+ * directions: ['left'],
311
+ * onSwipe: () => archive(item.id),
312
+ * onSwipeEnd: () => {
313
+ * swipe.x.value = withSpring(0)
314
+ * },
315
+ * })
316
+ *
317
+ * return (
318
+ * <GestureDetector gesture={swipe.gesture}>
319
+ * <Animated.View style={style} />
320
+ * </GestureDetector>
321
+ * )
322
+ * ```
323
+ *
324
+ * A swipe is a pan that is judged at release. The finger moves, `x` and `y`
325
+ * report it so the view can follow, and on release the travel and the speed
326
+ * along the dominant axis decide whether it counted. `onSwipe` fires only for
327
+ * a release that counted, in a direction `directions` allows.
328
+ *
329
+ * **Narrowing `directions` is the coexistence setting, not only a filter.**
330
+ * An all-horizontal list gives the gesture a directional threshold on x, so a
331
+ * vertical scroll view under it keeps working without a relation. A mixed
332
+ * list has no axis to lock, so the threshold is radial and the swipe competes
333
+ * for every touch — declare `deferTo` or `blocks` there.
334
+ *
335
+ * **Two thresholds, and they mean different things.** `threshold` is when the
336
+ * swipe takes the touch and starts reporting. `commitDistance` and
337
+ * `commitSpeed` are what a release is measured against. A swipe that
338
+ * activates and stops short reaches `onSwipeEnd` with a `null` direction, and
339
+ * never reaches `onSwipe`.
340
+ *
341
+ * **No style, and no animation.** This hook returns numbers and stops there.
342
+ * Whether a committed row leaves the screen and an uncommitted one springs
343
+ * back is the consumer's decision, taken in `onSwipeEnd`.
344
+ * `@rootnative/inertia-gestures` has a `useSwipe` that owns both, and needs
345
+ * `@rootnative/inertia` to do it.
346
+ *
347
+ * **Web.** RNGH recognizes pan from pointer events, so a mouse drag commits
348
+ * the same way a touch does, and `speed` is reported in the same units. A
349
+ * trackpad's two-finger swipe is a scroll, not a pan, and never reaches this
350
+ * hook.
351
+ *
352
+ * **Accessibility.** A swipe is invisible to a screen reader and unreachable
353
+ * from a keyboard. Whatever it commits must be reachable another way: a
354
+ * visible button for a row action, a paging control for a carousel, or
355
+ * `accessibilityActions` with `onAccessibilityAction`. A swipe-only action is
356
+ * a bug, not a trade-off.
357
+ *
358
+ * @param options - Directions, activation and commit criteria, callbacks, and
359
+ * the `alongside` / `blocks` / `deferTo` coexistence options every Impulse
360
+ * hook accepts.
361
+ */
362
+ export function useSwipe(options: UseSwipeOptions = {}): UseSwipeResult {
363
+ const {
364
+ threshold = DEFAULT_THRESHOLD,
365
+ commitDistance = DEFAULT_COMMIT_DISTANCE,
366
+ commitSpeed = DEFAULT_COMMIT_SPEED,
367
+ failOffset,
368
+ pointers,
369
+ enabled,
370
+ onSwipe,
371
+ onSwipeEnd,
372
+ onBegin,
373
+ onUpdate,
374
+ onFinalize,
375
+ } = options
376
+
377
+ const x = useSharedValue(0)
378
+ const y = useSharedValue(0)
379
+ const isActive = useSharedValue(false)
380
+ // RNGH's translation at the moment the swipe activated. Every number this
381
+ // hook reports is measured from here, so the `threshold` travel is not
382
+ // counted towards `commitDistance`.
383
+ const originX = useSharedValue(0)
384
+ const originY = useSharedValue(0)
385
+
386
+ const hitSlop = useStableRecord(options.hitSlop)
387
+ // Written inline at almost every call site, so compared by content rather
388
+ // than by identity — an array literal would otherwise rebuild the gesture
389
+ // on every render.
390
+ const directions = useStableList(options.directions ?? ALL_DIRECTIONS)
391
+
392
+ // Pulled apart into booleans so the commit worklet captures four primitives
393
+ // rather than the array. A captured array is serialized to the UI thread on
394
+ // every rebuild; four booleans are not.
395
+ const allowLeft = directions.includes('left')
396
+ const allowRight = directions.includes('right')
397
+ const allowUp = directions.includes('up')
398
+ const allowDown = directions.includes('down')
399
+
400
+ // The axis the directions live on, or `null` for a mixed list. This is what
401
+ // turns `directions` into an activation criterion: an axis means a
402
+ // directional threshold, which is what lets the swipe share a view with a
403
+ // scroller. A list with no allowed direction at all has no axis either, and
404
+ // falls back to the radial form — the gesture then tracks and never
405
+ // commits, which is what an empty list asks for.
406
+ const hasHorizontal = allowLeft || allowRight
407
+ const hasVertical = allowUp || allowDown
408
+ const axis =
409
+ hasHorizontal && !hasVertical
410
+ ? 'x'
411
+ : hasVertical && !hasHorizontal
412
+ ? 'y'
413
+ : null
414
+
415
+ // JS-thread callbacks reach the gesture through stable identities, so they
416
+ // are never gesture dependencies.
417
+ const handleSwipe = useLatestCallback(onSwipe)
418
+ const handleSwipeEnd = useLatestCallback(onSwipeEnd)
419
+ // Attaching a handler is not the same as calling it: RNGH decides which
420
+ // thread a gesture's callbacks run on by inspecting the ones it was given.
421
+ const hasSwipe = onSwipe !== undefined
422
+ const hasSwipeEnd = onSwipeEnd !== undefined
423
+
424
+ const built = useGestureMemo(
425
+ 'useSwipe',
426
+ () => {
427
+ /**
428
+ * Shape RNGH's flat event into the swipe payload.
429
+ *
430
+ * `direction` is passed in rather than derived here: it is decided once
431
+ * per release, and a payload built for `onUpdate` has no release to
432
+ * judge.
433
+ */
434
+ const toSwipeEvent = (
435
+ event:
436
+ | GestureStateChangeEvent<PanGestureHandlerEventPayload>
437
+ | GestureUpdateEvent<PanGestureHandlerEventPayload>,
438
+ direction: SwipeDirection | null,
439
+ ): SwipeEvent => {
440
+ 'worklet'
441
+ const translationX = x.value
442
+ const translationY = y.value
443
+ const horizontal = Math.abs(translationX) >= Math.abs(translationY)
444
+ return {
445
+ direction,
446
+ distance: Math.abs(horizontal ? translationX : translationY),
447
+ speed: Math.abs(horizontal ? event.velocityX : event.velocityY),
448
+ translation: { x: translationX, y: translationY },
449
+ velocity: { x: event.velocityX, y: event.velocityY },
450
+ absolute: { x: event.absoluteX, y: event.absoluteY },
451
+ pointers: event.numberOfPointers,
452
+ }
453
+ }
454
+
455
+ const pan = Gesture.Pan()
456
+ .onBegin((event) => {
457
+ 'worklet'
458
+ onBegin?.(toSwipeEvent(event, null))
459
+ })
460
+ .onStart((event) => {
461
+ 'worklet'
462
+ originX.value = event.translationX
463
+ originY.value = event.translationY
464
+ x.value = 0
465
+ y.value = 0
466
+ isActive.value = true
467
+ })
468
+ .onUpdate((event) => {
469
+ 'worklet'
470
+ x.value = event.translationX - originX.value
471
+ y.value = event.translationY - originY.value
472
+ onUpdate?.(toSwipeEvent(event, null))
473
+ })
474
+ .onEnd((event, success) => {
475
+ 'worklet'
476
+ // A cancelled gesture never commits. The finger did not lift, so
477
+ // the velocity describes the last movement rather than a release,
478
+ // and committing on it would act on a swipe the user never
479
+ // finished.
480
+ const direction = success
481
+ ? pickDirection(
482
+ x.value,
483
+ y.value,
484
+ event.velocityX,
485
+ event.velocityY,
486
+ commitDistance,
487
+ commitSpeed,
488
+ allowLeft,
489
+ allowRight,
490
+ allowUp,
491
+ allowDown,
492
+ )
493
+ : null
494
+ if (direction !== null && hasSwipe) {
495
+ scheduleOnRN(
496
+ handleSwipe,
497
+ toSwipeEvent(event, direction) as CommittedSwipeEvent,
498
+ )
499
+ }
500
+ // Fires on both endings, which is what a view that followed the
501
+ // finger needs: `onSwipe` says to act, this says where to put the
502
+ // view. RNGH reaches `onEnd` only from the ACTIVE state, so a touch
503
+ // that never passed the threshold never gets here.
504
+ if (hasSwipeEnd) {
505
+ scheduleOnRN(handleSwipeEnd, toSwipeEvent(event, direction), {
506
+ cancelled: !success,
507
+ })
508
+ }
509
+ })
510
+ .onFinalize((event, success) => {
511
+ 'worklet'
512
+ isActive.value = false
513
+ onFinalize?.(toSwipeEvent(event, null), success)
514
+ })
515
+
516
+ // The directional threshold the allowed directions imply. This is the
517
+ // part that lets a horizontal swipe live inside a vertical scroller:
518
+ // vertical movement never reaches the offset, so the swipe never claims
519
+ // the touch.
520
+ if (axis === 'x') {
521
+ pan.activeOffsetX([-threshold, threshold])
522
+ } else if (axis === 'y') {
523
+ pan.activeOffsetY([-threshold, threshold])
524
+ } else {
525
+ pan.minDistance(threshold)
526
+ }
527
+
528
+ // Applied conditionally rather than with a default, so an option the
529
+ // consumer did not set leaves RNGH's own default in place instead of
530
+ // Impulse overwriting it with a guess.
531
+ if (failOffset !== undefined) {
532
+ if (axis === 'x') {
533
+ pan.failOffsetY([-failOffset, failOffset])
534
+ } else if (axis === 'y') {
535
+ pan.failOffsetX([-failOffset, failOffset])
536
+ }
537
+ }
538
+ if (pointers !== undefined) {
539
+ pan.minPointers(pointers).maxPointers(pointers)
540
+ }
541
+ if (hitSlop !== undefined) {
542
+ pan.hitSlop(hitSlop)
543
+ }
544
+ if (enabled !== undefined) {
545
+ pan.enabled(enabled)
546
+ }
547
+ return pan
548
+ },
549
+ [
550
+ axis,
551
+ threshold,
552
+ commitDistance,
553
+ commitSpeed,
554
+ failOffset,
555
+ pointers,
556
+ hitSlop,
557
+ enabled,
558
+ allowLeft,
559
+ allowRight,
560
+ allowUp,
561
+ allowDown,
562
+ hasSwipe,
563
+ hasSwipeEnd,
564
+ handleSwipe,
565
+ handleSwipeEnd,
566
+ x,
567
+ y,
568
+ originX,
569
+ originY,
570
+ isActive,
571
+ onBegin,
572
+ onUpdate,
573
+ onFinalize,
574
+ ],
575
+ options,
576
+ )
577
+
578
+ // Memoised so a consumer can put the whole hook result in a dependency
579
+ // list. The shared values are stable for the life of the hook, so `built`
580
+ // is the only real input.
581
+ return useMemo(
582
+ () => buildIntentResult(built, { x, y, isActive }),
583
+ [built, x, y, isActive],
584
+ )
585
+ }