@rootnative/impulse 0.0.0-alpha.0

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 (44) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/LICENSE +21 -0
  3. package/README.md +174 -0
  4. package/dist/chunk-5BMRKYVY.js +39 -0
  5. package/dist/chunk-F4RHM4ZK.js +77 -0
  6. package/dist/chunk-FR242SUF.js +174 -0
  7. package/dist/chunk-IG5RXCYR.js +74 -0
  8. package/dist/chunk-LM645QQT.js +37 -0
  9. package/dist/chunk-PMR25UCT.js +8 -0
  10. package/dist/chunk-ZX7WNICB.js +39 -0
  11. package/dist/compose/index.d.ts +80 -0
  12. package/dist/compose/index.js +2 -0
  13. package/dist/drag/index.d.ts +277 -0
  14. package/dist/drag/index.js +4 -0
  15. package/dist/gesture-handler/index.d.ts +1 -0
  16. package/dist/gesture-handler/index.js +1 -0
  17. package/dist/index.d.ts +9 -0
  18. package/dist/index.js +8 -0
  19. package/dist/raw/index.d.ts +63 -0
  20. package/dist/raw/index.js +3 -0
  21. package/dist/tap/index.d.ts +150 -0
  22. package/dist/tap/index.js +4 -0
  23. package/dist/types-Ch2HM3aP.d.ts +142 -0
  24. package/dist/useGestureMemo-Ccv8rB0C.d.ts +38 -0
  25. package/jest-preset.cjs +60 -0
  26. package/jest-setup.cjs +56 -0
  27. package/package.json +115 -0
  28. package/src/compose/index.ts +7 -0
  29. package/src/compose/useGestures.ts +136 -0
  30. package/src/gesture-handler/index.ts +18 -0
  31. package/src/index.ts +53 -0
  32. package/src/intents/drag/index.ts +8 -0
  33. package/src/intents/tap/index.ts +2 -0
  34. package/src/intents/useDrag.ts +563 -0
  35. package/src/intents/useTap.ts +285 -0
  36. package/src/internal/useGestureMemo.ts +110 -0
  37. package/src/internal/useLatestCallback.ts +64 -0
  38. package/src/internal/useStableList.ts +62 -0
  39. package/src/internal/useStableRecord.ts +72 -0
  40. package/src/internal/warnOnce.ts +60 -0
  41. package/src/raw/index.ts +2 -0
  42. package/src/raw/useRawGesture.ts +71 -0
  43. package/src/relations/index.ts +120 -0
  44. package/src/types.ts +187 -0
@@ -0,0 +1,563 @@
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 {
10
+ runOnJS,
11
+ useSharedValue,
12
+ type SharedValue,
13
+ } from 'react-native-reanimated'
14
+ import {
15
+ useGestureMemo,
16
+ type GestureMemoOptions,
17
+ } from '../internal/useGestureMemo'
18
+ import { useLatestCallback } from '../internal/useLatestCallback'
19
+ import { useStableRecord } from '../internal/useStableRecord'
20
+ import { type HitSlop, type IntentResult, type Point } from '../types'
21
+
22
+ /**
23
+ * How far the finger must travel before the drag takes the touch, in points.
24
+ *
25
+ * Impulse's number, not RNGH's: a bare `Gesture.Pan()` activates almost
26
+ * immediately, which is what makes a drag inside a scroll view steal the
27
+ * scroll. A threshold is the single setting that decides whether those two
28
+ * can share a view, so it is set here rather than left to the consumer to
29
+ * discover.
30
+ *
31
+ * **This number is a design intention, not a measurement.** No hardware pass
32
+ * has happened. See Known gaps in CLAUDE.md.
33
+ */
34
+ const DEFAULT_THRESHOLD = 10
35
+
36
+ /** Which way the drag is allowed to move. */
37
+ export type DragAxis = 'x' | 'y' | 'both'
38
+
39
+ /**
40
+ * How far the drag may travel, in the same coordinates as `x` and `y`.
41
+ *
42
+ * Every edge is optional, and an omitted edge is unbounded. The names are the
43
+ * edges of the travel, not of the view: `left` is the smallest `x`, `bottom`
44
+ * is the largest `y`.
45
+ */
46
+ export interface DragBounds {
47
+ /** Smallest `x`. */
48
+ left?: number
49
+ /** Largest `x`. */
50
+ right?: number
51
+ /** Smallest `y`. */
52
+ top?: number
53
+ /** Largest `y`. */
54
+ bottom?: number
55
+ }
56
+
57
+ /** The intent-shaped payload a {@link useDrag} callback receives. */
58
+ export interface DragEvent {
59
+ /**
60
+ * Where the drag is now — the same numbers as `x` and `y`, after `bounds`
61
+ * and `elastic` were applied.
62
+ *
63
+ * This accumulates across gestures. A second drag continues from where the
64
+ * first one stopped, which is why `bounds` can be written against a layout
65
+ * rather than against one gesture's travel.
66
+ */
67
+ readonly position: Point
68
+ /**
69
+ * How far the finger moved since this gesture activated, raw.
70
+ *
71
+ * Untouched by `bounds` and `elastic`, and reset to zero at the start of
72
+ * every gesture. Read `position` for where the thing being dragged actually
73
+ * sits.
74
+ */
75
+ readonly translation: Point
76
+ /** Finger speed, in points per second. The input a release spring needs. */
77
+ readonly velocity: Point
78
+ /** The touch point relative to the window. */
79
+ readonly absolute: Point
80
+ /**
81
+ * The nearest point inside `bounds`. Equal to `position` whenever the drag
82
+ * is in bounds, which with the default `elastic` of `0` is always.
83
+ *
84
+ * With `elastic` set, the finger can pull the value past an edge and
85
+ * Impulse leaves it there on release — moving it back is an animation, and
86
+ * Impulse owns no animation vocabulary. This field is the destination that
87
+ * animation needs, so the consumer does not have to re-derive the clamp
88
+ * from bounds it already handed over.
89
+ */
90
+ readonly settled: Point
91
+ /** How many fingers are down. */
92
+ readonly pointers: number
93
+ }
94
+
95
+ /** Options for {@link useDrag}. */
96
+ export interface UseDragOptions extends GestureMemoOptions {
97
+ /**
98
+ * Which way the drag may move. Default `'both'`.
99
+ *
100
+ * The locked axis's shared value never changes — `axis: 'x'` leaves `y` at
101
+ * its initial value for the life of the hook. The axis also decides the
102
+ * activation criterion, which is the part that matters inside a scroll
103
+ * view: see `threshold`.
104
+ */
105
+ axis?: DragAxis
106
+ /**
107
+ * How far the finger must travel before the drag activates, in points.
108
+ * Default `10`.
109
+ *
110
+ * On a single axis this is a directional threshold, so a drag with
111
+ * `axis: 'x'` ignores vertical movement entirely and a vertical scroll view
112
+ * under it keeps working. On `'both'` it is a radial distance.
113
+ *
114
+ * Lower it for a drag that must feel immediate and owns its view. Raise it
115
+ * when the drag shares the view with something that should usually win.
116
+ */
117
+ threshold?: number
118
+ /**
119
+ * Movement across the axis that makes the drag fail, in points. Unset by
120
+ * default, which leaves RNGH's own behaviour in place.
121
+ *
122
+ * `threshold` decides when the drag wins; this decides when it gives up.
123
+ * Set it when the cross-axis gesture must win a diagonal — a horizontal
124
+ * row action inside a vertical list, where a mostly-vertical drag should
125
+ * scroll rather than half-open the row. Ignored when `axis` is `'both'`,
126
+ * which has no cross axis.
127
+ */
128
+ failOffset?: number
129
+ /**
130
+ * How far the drag may travel. Unbounded by default.
131
+ *
132
+ * Written inline as an object is fine — the gesture is not rebuilt when the
133
+ * contents are unchanged.
134
+ */
135
+ bounds?: DragBounds
136
+ /**
137
+ * How much of the finger's movement survives past a bound, `0` to `1`.
138
+ * Default `0`.
139
+ *
140
+ * `0` clamps hard at the edge. `1` ignores the bound while the finger is
141
+ * down. `0.3` or so gives the rubber-band pull an over-scroll has.
142
+ *
143
+ * **Impulse does not put the value back on release.** That is an animation,
144
+ * and Principle 5 keeps animation out of this library. `onDragEnd` carries
145
+ * `settled` — the point to animate to — and `@rootnative/inertia` or a
146
+ * plain `withSpring` does the moving.
147
+ */
148
+ elastic?: number
149
+ /**
150
+ * Where `x` and `y` start. Default `{ x: 0, y: 0 }`.
151
+ *
152
+ * Read once, when the hook mounts. Changing it later does nothing, because
153
+ * the shared values are the drag's state from then on — write
154
+ * `drag.x.value` to move it instead.
155
+ */
156
+ initial?: Point
157
+ /**
158
+ * How many fingers must be on the view. Unset by default, which leaves
159
+ * RNGH's range of one to ten in place.
160
+ *
161
+ * Setting it fixes the count exactly, so `pointers: 2` is a two-finger drag
162
+ * that neither starts with one finger nor survives a third.
163
+ */
164
+ pointers?: number
165
+ /**
166
+ * Extra touchable area around the view, in points.
167
+ *
168
+ * Written inline as an object is fine — the gesture is not rebuilt when the
169
+ * contents are unchanged.
170
+ */
171
+ hitSlop?: HitSlop
172
+ /**
173
+ * Whether the gesture is recognized at all. Default `true`.
174
+ *
175
+ * Prefer this over unmounting the `<GestureDetector>`: a disabled gesture
176
+ * keeps its identity and its relations, so re-enabling it does not
177
+ * re-attach anything.
178
+ */
179
+ enabled?: boolean
180
+ /**
181
+ * The drag passed `threshold` and now owns the touch. **Runs on the JS
182
+ * thread** — Impulse owns the `runOnJS` boundary, so this is an ordinary
183
+ * function and may touch React state.
184
+ *
185
+ * This is the first moment the drag has definitely won. `onBegin` fires
186
+ * earlier and promises nothing.
187
+ */
188
+ onDragStart?: (event: DragEvent) => void
189
+ /**
190
+ * The finger lifted and the drag is over. **Runs on the JS thread.**
191
+ *
192
+ * Fires only for a drag that activated and then released. A drag the system
193
+ * took away — a competing gesture won, or the app went to the background —
194
+ * reaches `onFinalize` with `success: false` and never gets here, because
195
+ * there was no release and so no velocity worth seeding a spring with.
196
+ *
197
+ * Read `velocity` to seed that release animation, and `settled` for where
198
+ * an elastic overshoot should return to.
199
+ */
200
+ onDragEnd?: (event: DragEvent) => void
201
+ /**
202
+ * The finger went down and the gesture is now a candidate. **This is a
203
+ * worklet** — mark it with the `'worklet'` directive, and do not touch
204
+ * React state from it.
205
+ *
206
+ * Being a candidate is not the same as winning: the drag has not passed
207
+ * `threshold` yet and may never. Use it to show a grabbed state, and undo
208
+ * that state in `onFinalize`.
209
+ */
210
+ onBegin?: (event: DragEvent) => void
211
+ /**
212
+ * The drag moved. **This is a worklet**, and it runs on every frame the
213
+ * finger moves.
214
+ *
215
+ * There is no JS-thread counterpart on purpose. A per-frame `runOnJS` is a
216
+ * scheduling cost paid sixty times a second for a value that is already on
217
+ * the thread that needs it — read `x` and `y` from a `useAnimatedStyle`
218
+ * instead, and let this callback handle what the style cannot.
219
+ */
220
+ onUpdate?: (event: DragEvent) => void
221
+ /**
222
+ * The gesture is over, whether it activated or not. **This is a worklet.**
223
+ *
224
+ * `success` is `true` when the drag activated and ended normally. This is
225
+ * the right place to clear anything `onBegin` set, because it runs on both
226
+ * paths.
227
+ */
228
+ onFinalize?: (event: DragEvent, success: boolean) => void
229
+ }
230
+
231
+ /** What {@link useDrag} returns. */
232
+ export interface UseDragResult extends IntentResult<PanGesture> {
233
+ /**
234
+ * Where the drag is along the x axis, after `bounds` and `elastic`.
235
+ *
236
+ * Writable: assigning `drag.x.value` moves the drag, and the next gesture
237
+ * continues from the new number rather than snapping back. That is how a
238
+ * release animation hands control back — animate this value, and the drag
239
+ * picks up wherever the animation left it.
240
+ *
241
+ * Frozen at its initial value when `axis` is `'y'`.
242
+ */
243
+ readonly x: SharedValue<number>
244
+ /**
245
+ * Where the drag is along the y axis, after `bounds` and `elastic`.
246
+ *
247
+ * Writable, on the same terms as `x`. Frozen at its initial value when
248
+ * `axis` is `'x'`.
249
+ */
250
+ readonly y: SharedValue<number>
251
+ }
252
+
253
+ /**
254
+ * Hold a number inside a range, letting `elastic` of the excess through.
255
+ *
256
+ * A worklet, because the drag's whole value path runs on the UI thread.
257
+ * `elastic` of `0` is a hard clamp and `1` ignores the bound, so the two
258
+ * extremes are the two behaviours a consumer would otherwise write by hand.
259
+ */
260
+ function resist(
261
+ value: number,
262
+ min: number | undefined,
263
+ max: number | undefined,
264
+ elastic: number,
265
+ ): number {
266
+ 'worklet'
267
+ if (min !== undefined && value < min) {
268
+ return min + (value - min) * elastic
269
+ }
270
+ if (max !== undefined && value > max) {
271
+ return max + (value - max) * elastic
272
+ }
273
+ return value
274
+ }
275
+
276
+ /**
277
+ * The nearest number inside the range. What {@link resist} would have
278
+ * returned with `elastic` at `0`.
279
+ */
280
+ function clamp(
281
+ value: number,
282
+ min: number | undefined,
283
+ max: number | undefined,
284
+ ): number {
285
+ 'worklet'
286
+ if (min !== undefined && value < min) {
287
+ return min
288
+ }
289
+ if (max !== undefined && value > max) {
290
+ return max
291
+ }
292
+ return value
293
+ }
294
+
295
+ /**
296
+ * Recognize a drag, and stream where it is.
297
+ *
298
+ * ```tsx
299
+ * const drag = useDrag({ axis: 'x', bounds: { left: -120, right: 0 } })
300
+ * const style = useAnimatedStyle(() => ({
301
+ * transform: [{ translateX: drag.x.value }],
302
+ * }))
303
+ *
304
+ * return (
305
+ * <GestureDetector gesture={drag.gesture}>
306
+ * <Animated.View style={style} />
307
+ * </GestureDetector>
308
+ * )
309
+ * ```
310
+ *
311
+ * `x` and `y` are shared values, so the view follows the finger on the UI
312
+ * thread with no re-render. They accumulate across gestures: a second drag
313
+ * continues from where the first stopped.
314
+ *
315
+ * **No style, and no animation.** This hook returns numbers and stops there.
316
+ * Building the `transform` is the consumer's call, and so is any spring back.
317
+ * `@rootnative/inertia-gestures` has a `useDrag` that does both, and needs
318
+ * `@rootnative/inertia` to do it — reach for that one when a `Motion.View`
319
+ * should follow a finger and spring home, and for this one when the values
320
+ * are what you want.
321
+ *
322
+ * **Activation criteria.** `threshold` defaults to 10 points. With
323
+ * `axis: 'x'` or `'y'` it is directional, so a horizontal drag inside a
324
+ * vertical `ScrollView` leaves the scroll alone until the finger commits
325
+ * sideways; with `'both'` it is a radial distance. Set `failOffset` as well
326
+ * when a mostly-diagonal move should go to the other gesture rather than to
327
+ * this one. The default has not been measured on hardware yet.
328
+ *
329
+ * **Coexistence.** A threshold decides who moves first; it does not decide
330
+ * who wins a contested touch. For a drag inside a scroll view, say which one
331
+ * the touch belongs to as well — `deferTo: scrollRef` for a drag that is the
332
+ * fallback, `blocks: listRef` for one that is the foreground affordance.
333
+ *
334
+ * **Web.** RNGH recognizes pan from pointer events, so a mouse drag behaves
335
+ * the same as a touch drag and `velocity` is reported in the same units. A
336
+ * trackpad's momentum scroll is not a pan and never reaches this hook.
337
+ * `pointers` above 1 is unreliable on web.
338
+ *
339
+ * **Accessibility.** A drag is invisible to a screen reader and unreachable
340
+ * from a keyboard, and this hook does not fix that. Whatever the drag
341
+ * adjusts must be reachable another way: a pair of buttons for a slider, a
342
+ * visible action for a swipeable row, or `accessibilityActions` with
343
+ * `onAccessibilityAction` — `increment` and `decrement` for a value,
344
+ * `magicTap` or a named action for a dismissal. A drag-only affordance is a
345
+ * bug, not a trade-off.
346
+ *
347
+ * @param options - Activation criteria, bounds, callbacks, and the
348
+ * `alongside` / `blocks` / `deferTo` coexistence options every Impulse hook
349
+ * accepts.
350
+ */
351
+ export function useDrag(options: UseDragOptions = {}): UseDragResult {
352
+ const {
353
+ axis = 'both',
354
+ threshold = DEFAULT_THRESHOLD,
355
+ failOffset,
356
+ elastic = 0,
357
+ pointers,
358
+ enabled,
359
+ onDragStart,
360
+ onDragEnd,
361
+ onBegin,
362
+ onUpdate,
363
+ onFinalize,
364
+ } = options
365
+
366
+ // Read once. `useSharedValue` keeps its first argument and ignores every
367
+ // later one, and that is the behaviour the option documents: after mount
368
+ // the shared values are the drag's state, and the consumer moves it by
369
+ // writing them.
370
+ const x = useSharedValue(options.initial?.x ?? 0)
371
+ const y = useSharedValue(options.initial?.y ?? 0)
372
+ const isActive = useSharedValue(false)
373
+ // Where `x` and `y` were when this gesture activated, so the position can
374
+ // be rebuilt from the start plus RNGH's per-gesture translation.
375
+ const startX = useSharedValue(0)
376
+ const startY = useSharedValue(0)
377
+
378
+ // The two options a consumer writes as object literals, so the two that
379
+ // would rebuild the gesture every render if taken as-is.
380
+ const bounds = useStableRecord(options.bounds)
381
+ const hitSlop = useStableRecord(options.hitSlop)
382
+
383
+ // Pulled out of `bounds` so the worklets below capture four primitives
384
+ // rather than the record. A captured object is serialized to the UI thread
385
+ // on every rebuild; four numbers are not.
386
+ const left = bounds?.left
387
+ const right = bounds?.right
388
+ const top = bounds?.top
389
+ const bottom = bounds?.bottom
390
+
391
+ const movesX = axis !== 'y'
392
+ const movesY = axis !== 'x'
393
+
394
+ // JS-thread callbacks reach the gesture through stable identities, so they
395
+ // are never gesture dependencies. The worklet callbacks stay direct
396
+ // dependencies, because a worklet is captured as written.
397
+ const handleDragStart = useLatestCallback(onDragStart)
398
+ const handleDragEnd = useLatestCallback(onDragEnd)
399
+ // Attaching a handler is not the same as calling it: RNGH decides which
400
+ // thread a gesture's callbacks run on by inspecting the ones it was given,
401
+ // so the gesture does have to change when a handler appears or disappears.
402
+ // Booleans, so they change only when that is actually true.
403
+ const hasDragStart = onDragStart !== undefined
404
+ const hasDragEnd = onDragEnd !== undefined
405
+
406
+ const built = useGestureMemo(
407
+ 'useDrag',
408
+ () => {
409
+ /**
410
+ * Shape RNGH's flat event into the drag payload.
411
+ *
412
+ * Built inside the gesture rather than at module scope because it
413
+ * closes over the bounds — which is also why it is rebuilt only when
414
+ * they change. Called from the callbacks, and only when one is present:
415
+ * a payload nobody reads is six objects allocated on the UI thread for
416
+ * every frame of every drag.
417
+ */
418
+ const toDragEvent = (
419
+ event:
420
+ | GestureStateChangeEvent<PanGestureHandlerEventPayload>
421
+ | GestureUpdateEvent<PanGestureHandlerEventPayload>,
422
+ ): DragEvent => {
423
+ 'worklet'
424
+ const position = { x: x.value, y: y.value }
425
+ return {
426
+ position,
427
+ translation: { x: event.translationX, y: event.translationY },
428
+ velocity: { x: event.velocityX, y: event.velocityY },
429
+ absolute: { x: event.absoluteX, y: event.absoluteY },
430
+ settled: {
431
+ x: clamp(position.x, left, right),
432
+ y: clamp(position.y, top, bottom),
433
+ },
434
+ pointers: event.numberOfPointers,
435
+ }
436
+ }
437
+
438
+ const pan = Gesture.Pan()
439
+ .onBegin((event) => {
440
+ 'worklet'
441
+ onBegin?.(toDragEvent(event))
442
+ })
443
+ .onStart((event) => {
444
+ 'worklet'
445
+ // The finger has already travelled `threshold` by the time RNGH
446
+ // calls this, and that distance is in `translation` from here on.
447
+ // Subtracting it now means the first `onUpdate` reproduces the
448
+ // current position instead of adding the threshold to it — the
449
+ // difference between a drag that picks the view up where it is and
450
+ // one that jumps ten points before it moves.
451
+ startX.value = x.value - event.translationX
452
+ startY.value = y.value - event.translationY
453
+ isActive.value = true
454
+ if (hasDragStart) {
455
+ runOnJS(handleDragStart)(toDragEvent(event))
456
+ }
457
+ })
458
+ .onUpdate((event) => {
459
+ 'worklet'
460
+ if (movesX) {
461
+ x.value = resist(
462
+ startX.value + event.translationX,
463
+ left,
464
+ right,
465
+ elastic,
466
+ )
467
+ }
468
+ if (movesY) {
469
+ y.value = resist(
470
+ startY.value + event.translationY,
471
+ top,
472
+ bottom,
473
+ elastic,
474
+ )
475
+ }
476
+ onUpdate?.(toDragEvent(event))
477
+ })
478
+ .onEnd((event, success) => {
479
+ 'worklet'
480
+ // Guarded on `success`, because RNGH calls this for a cancelled
481
+ // drag too. A cancelled drag had its touch taken away — by a
482
+ // competing gesture winning, or the app going to the background —
483
+ // and there was no release, so there is no velocity worth seeding a
484
+ // spring with. That path reaches `onFinalize` instead.
485
+ if (success && hasDragEnd) {
486
+ runOnJS(handleDragEnd)(toDragEvent(event))
487
+ }
488
+ })
489
+ .onFinalize((event, success) => {
490
+ 'worklet'
491
+ isActive.value = false
492
+ onFinalize?.(toDragEvent(event), success)
493
+ })
494
+
495
+ // A directional threshold on a single axis, a radial one on both. The
496
+ // directional form is what lets a horizontal drag and a vertical
497
+ // scroller share a view: vertical movement never reaches the offset, so
498
+ // the drag never claims the touch.
499
+ if (axis === 'x') {
500
+ pan.activeOffsetX([-threshold, threshold])
501
+ } else if (axis === 'y') {
502
+ pan.activeOffsetY([-threshold, threshold])
503
+ } else {
504
+ pan.minDistance(threshold)
505
+ }
506
+
507
+ // Applied conditionally rather than with a default, so an option the
508
+ // consumer did not set leaves RNGH's own default in place instead of
509
+ // Impulse overwriting it with a guess.
510
+ if (failOffset !== undefined) {
511
+ if (axis === 'x') {
512
+ pan.failOffsetY([-failOffset, failOffset])
513
+ } else if (axis === 'y') {
514
+ pan.failOffsetX([-failOffset, failOffset])
515
+ }
516
+ }
517
+ if (pointers !== undefined) {
518
+ pan.minPointers(pointers).maxPointers(pointers)
519
+ }
520
+ if (hitSlop !== undefined) {
521
+ pan.hitSlop(hitSlop)
522
+ }
523
+ if (enabled !== undefined) {
524
+ pan.enabled(enabled)
525
+ }
526
+ return pan
527
+ },
528
+ [
529
+ axis,
530
+ threshold,
531
+ failOffset,
532
+ left,
533
+ right,
534
+ top,
535
+ bottom,
536
+ elastic,
537
+ pointers,
538
+ hitSlop,
539
+ enabled,
540
+ movesX,
541
+ movesY,
542
+ hasDragStart,
543
+ hasDragEnd,
544
+ handleDragStart,
545
+ handleDragEnd,
546
+ x,
547
+ y,
548
+ startX,
549
+ startY,
550
+ isActive,
551
+ onBegin,
552
+ onUpdate,
553
+ onFinalize,
554
+ ],
555
+ options,
556
+ )
557
+
558
+ // Memoised for the same reason `useGestureMemo` memoises its own result: a
559
+ // consumer may put the whole hook result in a dependency list, and a fresh
560
+ // object every render would make that dependency useless. The shared values
561
+ // are stable for the life of the hook, so `built` is the only real input.
562
+ return useMemo(() => ({ ...built, x, y, isActive }), [built, x, y, isActive])
563
+ }