@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,507 @@
1
+ import { useMemo } from 'react'
2
+ import {
3
+ Gesture,
4
+ type GestureStateChangeEvent,
5
+ type GestureUpdateEvent,
6
+ type RotationGesture,
7
+ type RotationGestureHandlerEventPayload,
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 { useStableRecord } from '../internal/useStableRecord'
18
+ import {
19
+ type HitSlop,
20
+ type IntentEndInfo,
21
+ type IntentResult,
22
+ type Point,
23
+ } from '../types'
24
+
25
+ /**
26
+ * Radians to degrees.
27
+ *
28
+ * **Impulse reports degrees; RNGH reports radians.** This is the one place
29
+ * the two differ, and it is deliberate: a range written as `min: -45` is a
30
+ * range a person wrote, and `-Math.PI / 4` is one they derived. A style takes
31
+ * either unit — `${angle}deg` and `${angle}rad` are both valid — so the
32
+ * conversion costs a consumer nothing and saves them the derivation.
33
+ */
34
+ const DEGREES_PER_RADIAN = 180 / Math.PI
35
+
36
+ /** The intent-shaped payload a {@link useRotate} callback receives. */
37
+ export interface RotateEvent {
38
+ /**
39
+ * How far the thing is turned now, in **degrees**, after `min`, `max` and
40
+ * `elastic` were applied.
41
+ *
42
+ * This accumulates across gestures. A second rotation continues from where
43
+ * the first one stopped, which is why `min` and `max` can be written as the
44
+ * travel of the whole control rather than of one gesture.
45
+ *
46
+ * Positive is clockwise, which is what React Native's `rotate` transform
47
+ * also treats as positive.
48
+ */
49
+ readonly angle: number
50
+ /**
51
+ * How far the fingers turned during **this gesture alone**, in degrees.
52
+ *
53
+ * Untouched by `min`, `max` and `elastic`, and reset to zero at the start of
54
+ * every gesture. This is RNGH's own `rotation`, converted. Read `angle` for
55
+ * how far the thing being turned actually sits.
56
+ */
57
+ readonly gestureAngle: number
58
+ /**
59
+ * The point the rotation turns about — RNGH's anchor, the centre between
60
+ * the fingers, relative to the view.
61
+ *
62
+ * A rotation applied about the view's own centre turns the content under
63
+ * the fingers rather than with them. This is the point the transform has to
64
+ * pivot on, and it is the rotation counterpart of `usePinch`'s `focal`.
65
+ */
66
+ readonly anchor: Point
67
+ /**
68
+ * How fast the angle is changing, in degrees per second.
69
+ *
70
+ * RNGH's own documentation calls this "point units per second" for the
71
+ * rotation handler, which is a copy of the pan handler's wording rather
72
+ * than a description of this value. It is an angular speed.
73
+ */
74
+ readonly velocity: number
75
+ /**
76
+ * The nearest angle inside `min` and `max`. Equal to `angle` whenever the
77
+ * rotation is in range, which with the default `elastic` of `0` is always.
78
+ *
79
+ * With `elastic` set, the fingers can turn past an end and Impulse leaves
80
+ * the angle there on release — moving it back is an animation, and Impulse
81
+ * owns no animation vocabulary. This field is the destination that
82
+ * animation needs, so the consumer does not have to re-derive the clamp
83
+ * from a range it already handed over.
84
+ */
85
+ readonly settled: number
86
+ /** How many fingers are down. */
87
+ readonly pointers: number
88
+ }
89
+
90
+ /** Options for {@link useRotate}. */
91
+ export interface UseRotateOptions extends GestureMemoOptions {
92
+ /**
93
+ * The angle before any rotation, in degrees. Default `0`.
94
+ *
95
+ * Read once, at mount. `useSharedValue` keeps its first argument and
96
+ * ignores every later one, and that is the behaviour this option
97
+ * documents: after mount `angle` is the rotation's state, and the consumer
98
+ * moves it by writing it.
99
+ */
100
+ initial?: number
101
+ /**
102
+ * The smallest angle, in degrees. Unset by default, which lets the fingers
103
+ * turn the thing without limit.
104
+ *
105
+ * Impulse does not wrap the angle at a full turn. Without `min` and `max`
106
+ * a second turn reports 360 more degrees than the first, which is what a
107
+ * dial counting revolutions needs and what a photo editor does not.
108
+ */
109
+ min?: number
110
+ /**
111
+ * The largest angle, in degrees. Unset by default, which lets the fingers
112
+ * turn the thing without limit.
113
+ */
114
+ max?: number
115
+ /**
116
+ * How much of the travel past `min` or `max` reaches `angle`, from `0` to
117
+ * `1`. Default `0`.
118
+ *
119
+ * `0` stops dead at the end. `1` ignores the end while the fingers are
120
+ * down. Anything between is resistance — the rotation keeps turning and
121
+ * turns less than the fingers do.
122
+ *
123
+ * Impulse does not bring the angle back. The end callback carries
124
+ * `settled` — the angle to animate to — and `@rootnative/inertia` or a
125
+ * `withSpring` of your own does the rest.
126
+ */
127
+ elastic?: number
128
+ /**
129
+ * Extra touchable area around the view, in points.
130
+ *
131
+ * Written inline as an object is fine — the gesture is not rebuilt when the
132
+ * contents are unchanged.
133
+ */
134
+ hitSlop?: HitSlop
135
+ /**
136
+ * Whether the gesture is recognized at all. Default `true`.
137
+ *
138
+ * Prefer this over unmounting the `<GestureDetector>`: a disabled gesture
139
+ * keeps its identity and its relations, so re-enabling it does not
140
+ * re-attach anything.
141
+ */
142
+ enabled?: boolean
143
+ /**
144
+ * The rotation now owns the touch. **Runs on the JS thread** — Impulse owns
145
+ * the `scheduleOnRN` boundary, so this is an ordinary function and may
146
+ * touch React state.
147
+ *
148
+ * This is the first moment the rotation has definitely won. `onBegin` fires
149
+ * earlier and promises nothing.
150
+ */
151
+ onRotateStart?: (event: RotateEvent) => void
152
+ /**
153
+ * The rotation is over. **Runs on the JS thread.**
154
+ *
155
+ * Fires only for a rotation that activated, so a touch that never became
156
+ * one never reaches here on either path.
157
+ *
158
+ * It fires for both endings, and `cancelled` says which. `false` is the
159
+ * fingers lifting, and `velocity` then describes the release. `true` is the
160
+ * system taking the rotation away — a competing gesture won, or the app
161
+ * went to the background. **There was no release on that path, so the
162
+ * velocity describes the last movement rather than a throw.**
163
+ *
164
+ * Read `settled` on both paths for where an elastic overshoot belongs.
165
+ */
166
+ onRotateEnd?: (event: RotateEvent, info: IntentEndInfo) => void
167
+ /**
168
+ * A finger went down and the gesture is now a candidate. **This is a
169
+ * worklet** — mark it with the `'worklet'` directive, and do not touch
170
+ * React state from it.
171
+ *
172
+ * Being a candidate is not the same as winning: one finger cannot turn
173
+ * anything, and a second one may never arrive. Undo whatever it sets in
174
+ * `onFinalize`.
175
+ */
176
+ onBegin?: (event: RotateEvent) => void
177
+ /**
178
+ * The angle changed. **This is a worklet**, and it runs on every frame the
179
+ * fingers turn.
180
+ *
181
+ * `angle` is already written by the time this runs, so a `useAnimatedStyle`
182
+ * reading it needs nothing from here. Use this for the work that turning
183
+ * alone does not do — snapping a readout to whole degrees, say.
184
+ *
185
+ * There is no JS-thread counterpart on purpose. A per-frame `scheduleOnRN`
186
+ * is a scheduling cost paid sixty times a second for a value that is
187
+ * already on the thread that needs it.
188
+ */
189
+ onUpdate?: (event: RotateEvent) => void
190
+ /**
191
+ * The gesture is over, whether it activated or not. **This is a worklet.**
192
+ *
193
+ * `success` is `true` when the rotation activated and ended normally. This
194
+ * is the right place to clear anything `onBegin` set, because it runs on
195
+ * both paths.
196
+ */
197
+ onFinalize?: (event: RotateEvent, success: boolean) => void
198
+ }
199
+
200
+ /** What {@link useRotate} returns. */
201
+ export interface UseRotateResult extends IntentResult<RotationGesture> {
202
+ /**
203
+ * How far the thing is turned, in **degrees**, after `min`, `max` and
204
+ * `elastic`.
205
+ *
206
+ * **This is a position, not a movement.** It accumulates across gestures,
207
+ * so a second rotation continues from where the first stopped. `usePan` is
208
+ * the hook whose value zeroes at every gesture.
209
+ *
210
+ * Writing it is allowed and is how a release animation, or a reset button,
211
+ * hands control back. The next rotation continues from whatever it holds.
212
+ */
213
+ readonly angle: SharedValue<number>
214
+ /**
215
+ * The point the rotation turns about, relative to the view.
216
+ *
217
+ * It keeps the last gesture's anchor after the fingers lift, so a release
218
+ * animation turns about the same point the gesture did rather than
219
+ * snapping to the view's centre.
220
+ */
221
+ readonly anchor: SharedValue<Point>
222
+ }
223
+
224
+ /**
225
+ * Hold an angle inside a range, letting `elastic` of the excess through.
226
+ *
227
+ * A worklet, because the rotation's whole value path runs on the UI thread.
228
+ * `elastic` of `0` is a hard clamp and `1` ignores the end, so the two
229
+ * extremes are the two behaviours a consumer would otherwise write by hand.
230
+ */
231
+ function resist(
232
+ value: number,
233
+ min: number | undefined,
234
+ max: number | undefined,
235
+ elastic: number,
236
+ ): number {
237
+ 'worklet'
238
+ if (min !== undefined && value < min) {
239
+ return min + (value - min) * elastic
240
+ }
241
+ if (max !== undefined && value > max) {
242
+ return max + (value - max) * elastic
243
+ }
244
+ return value
245
+ }
246
+
247
+ /**
248
+ * The nearest angle inside the range. What {@link resist} would have returned
249
+ * with `elastic` at `0`.
250
+ */
251
+ function clamp(
252
+ value: number,
253
+ min: number | undefined,
254
+ max: number | undefined,
255
+ ): number {
256
+ 'worklet'
257
+ if (min !== undefined && value < min) {
258
+ return min
259
+ }
260
+ if (max !== undefined && value > max) {
261
+ return max
262
+ }
263
+ return value
264
+ }
265
+
266
+ /**
267
+ * Recognize a two-finger rotation, and own the angle it produces.
268
+ *
269
+ * ```tsx
270
+ * const rotate = useRotate({ min: -45, max: 45 })
271
+ *
272
+ * const style = useAnimatedStyle(() => ({
273
+ * transform: [{ rotate: `${rotate.angle.value}deg` }],
274
+ * }))
275
+ *
276
+ * return (
277
+ * <GestureDetector gesture={rotate.gesture}>
278
+ * <Animated.Image style={style} source={source} />
279
+ * </GestureDetector>
280
+ * )
281
+ * ```
282
+ *
283
+ * **The angle is in degrees, and RNGH's is in radians.** This is the one
284
+ * place Impulse changes a unit rather than passing one through. A range
285
+ * written as `min: -45` is a range a person wrote; `-Math.PI / 4` is one they
286
+ * derived, and deriving it per project is the work this library exists to
287
+ * remove. A React Native style takes either unit, so the choice costs the
288
+ * consumer nothing. `gestureAngle` is the same conversion applied to RNGH's
289
+ * per-gesture value.
290
+ *
291
+ * **The angle accumulates; RNGH's does not.** A bare `Gesture.Rotation()`
292
+ * reports a value that restarts at zero on every gesture, so a control built
293
+ * on it springs back to upright the moment the fingers lift again. Carrying
294
+ * the angle across gestures is the stored start that every consumer otherwise
295
+ * writes, and it is why `min` and `max` can be the travel of the control
296
+ * rather than of one gesture.
297
+ *
298
+ * **It does not wrap at a full turn.** Without `min` and `max`, a second
299
+ * revolution reports 360 degrees more than the first. That is what a dial
300
+ * counting turns needs. A control that should read `10` rather than `370`
301
+ * either sets a range or takes the remainder itself — Impulse does not guess
302
+ * which, because both are correct for something.
303
+ *
304
+ * **`anchor` is the point the turn happens about.** Rotating about the view's
305
+ * own centre turns the content under the fingers rather than with them. It is
306
+ * the rotation counterpart of `usePinch`'s `focal`, and it is read at
307
+ * `onStart` as well as at every update so the first callback does not report
308
+ * the previous gesture's point.
309
+ *
310
+ * **Activation criteria.** There are none to set. RNGH's rotation takes the
311
+ * touch as soon as two fingers turn, and it exposes no threshold, so Impulse
312
+ * has none to pass on. `usePinch` is the other intent in this position.
313
+ *
314
+ * **Coexistence.** A rotation almost always shares its view with a pinch, and
315
+ * neither has a threshold to separate them with. Say it: `useGestures` with
316
+ * `mode: 'simultaneous'` for gestures this screen owns, `alongside` for one
317
+ * it does not.
318
+ *
319
+ * **Web.** RNGH recognizes rotation from pointer events, so it needs two
320
+ * pointers — a touchscreen, or a device that reports them. A trackpad's
321
+ * rotation is not a pointer pair and never reaches this hook, so a desktop
322
+ * browser with a trackpad alone cannot turn anything. Give it a control that
323
+ * writes `angle` directly, which the accessibility fallback needs anyway.
324
+ *
325
+ * **Accessibility.** A rotation is invisible to a screen reader and
326
+ * unreachable from a keyboard, and this hook does not fix that. Whatever the
327
+ * rotation turns must be reachable another way: buttons that step the angle
328
+ * by a documented amount, a control that resets it to zero, or
329
+ * `accessibilityActions` with `onAccessibilityAction`. A rotate-only
330
+ * affordance is a bug, not a trade-off.
331
+ *
332
+ * @param options - The angle range, callbacks, and the `alongside` /
333
+ * `blocks` / `deferTo` coexistence options every Impulse hook accepts.
334
+ */
335
+ export function useRotate(options: UseRotateOptions = {}): UseRotateResult {
336
+ const {
337
+ initial = 0,
338
+ min,
339
+ max,
340
+ elastic = 0,
341
+ enabled,
342
+ onRotateStart,
343
+ onRotateEnd,
344
+ onBegin,
345
+ onUpdate,
346
+ onFinalize,
347
+ } = options
348
+
349
+ const angle = useSharedValue(initial)
350
+ const anchor = useSharedValue<Point>({ x: 0, y: 0 })
351
+ const isActive = useSharedValue(false)
352
+ // What `angle` would be at a gesture rotation of zero, so the position can
353
+ // be rebuilt as `start + rotation` on every frame.
354
+ const startAngle = useSharedValue(initial)
355
+
356
+ const hitSlop = useStableRecord(options.hitSlop)
357
+
358
+ // JS-thread callbacks reach the gesture through stable identities, so they
359
+ // are never gesture dependencies. The worklet callbacks stay direct
360
+ // dependencies, because a worklet is captured as written.
361
+ const handleRotateStart = useLatestCallback(onRotateStart)
362
+ const handleRotateEnd = useLatestCallback(onRotateEnd)
363
+ // Attaching a handler is not the same as calling it: RNGH decides which
364
+ // thread a gesture's callbacks run on by inspecting the ones it was given,
365
+ // so the gesture does have to change when a handler appears or disappears.
366
+ const hasRotateStart = onRotateStart !== undefined
367
+ const hasRotateEnd = onRotateEnd !== undefined
368
+
369
+ const built = useGestureMemo(
370
+ 'useRotate',
371
+ () => {
372
+ /**
373
+ * Shape RNGH's flat event into the rotation payload.
374
+ *
375
+ * Built inside the gesture rather than at module scope because it
376
+ * closes over the range — which is also why it is rebuilt only when
377
+ * that changes. Called from the callbacks, and only when one is
378
+ * present: a payload nobody reads is three objects allocated on the UI
379
+ * thread for every frame of every rotation.
380
+ */
381
+ const toRotateEvent = (
382
+ event:
383
+ | GestureStateChangeEvent<RotationGestureHandlerEventPayload>
384
+ | GestureUpdateEvent<RotationGestureHandlerEventPayload>,
385
+ ): RotateEvent => {
386
+ 'worklet'
387
+ return {
388
+ angle: angle.value,
389
+ gestureAngle: event.rotation * DEGREES_PER_RADIAN,
390
+ anchor: anchor.value,
391
+ velocity: event.velocity * DEGREES_PER_RADIAN,
392
+ settled: clamp(angle.value, min, max),
393
+ pointers: event.numberOfPointers,
394
+ }
395
+ }
396
+
397
+ /**
398
+ * Record the anchor.
399
+ *
400
+ * A fresh object rather than two writes into the existing one: a
401
+ * shared value notifies on assignment, so mutating the point in place
402
+ * would leave a `useAnimatedStyle` reading a point that never changed.
403
+ *
404
+ * Called from `onStart` as well as `onUpdate`, so `onRotateStart`
405
+ * reports where the fingers are rather than where the previous gesture
406
+ * left them. Deliberately not called from `onEnd` or `onFinalize`: the
407
+ * fingers are lifting there and the point RNGH reports is no longer the
408
+ * one the turn happened about.
409
+ */
410
+ const trackAnchor = (
411
+ event:
412
+ | GestureStateChangeEvent<RotationGestureHandlerEventPayload>
413
+ | GestureUpdateEvent<RotationGestureHandlerEventPayload>,
414
+ ) => {
415
+ 'worklet'
416
+ anchor.value = { x: event.anchorX, y: event.anchorY }
417
+ }
418
+
419
+ const rotation = Gesture.Rotation()
420
+ .onBegin((event) => {
421
+ 'worklet'
422
+ onBegin?.(toRotateEvent(event))
423
+ })
424
+ .onStart((event) => {
425
+ 'worklet'
426
+ // Subtract rather than assign. RNGH reports zero here in the
427
+ // ordinary case, but it activates on movement, so the fingers may
428
+ // already have turned. Subtracting means the first `onUpdate`
429
+ // reproduces the current angle instead of adding that head start to
430
+ // it — the same correction `useDrag` applies to its threshold.
431
+ startAngle.value = angle.value - event.rotation * DEGREES_PER_RADIAN
432
+ trackAnchor(event)
433
+ isActive.value = true
434
+ if (hasRotateStart) {
435
+ scheduleOnRN(handleRotateStart, toRotateEvent(event))
436
+ }
437
+ })
438
+ .onUpdate((event) => {
439
+ 'worklet'
440
+ trackAnchor(event)
441
+ angle.value = resist(
442
+ startAngle.value + event.rotation * DEGREES_PER_RADIAN,
443
+ min,
444
+ max,
445
+ elastic,
446
+ )
447
+ onUpdate?.(toRotateEvent(event))
448
+ })
449
+ .onEnd((event, success) => {
450
+ 'worklet'
451
+ // Not guarded on `success`: RNGH calls this for a cancelled
452
+ // rotation too, and `cancelled` is what carries that to the JS
453
+ // thread. RNGH reaches `onEnd` only from the ACTIVE state, so a
454
+ // touch that never became a rotation goes to `onFinalize` and never
455
+ // gets here.
456
+ if (hasRotateEnd) {
457
+ scheduleOnRN(handleRotateEnd, toRotateEvent(event), {
458
+ cancelled: !success,
459
+ })
460
+ }
461
+ })
462
+ .onFinalize((event, success) => {
463
+ 'worklet'
464
+ isActive.value = false
465
+ onFinalize?.(toRotateEvent(event), success)
466
+ })
467
+
468
+ // Applied conditionally rather than with a default, so an option the
469
+ // consumer did not set leaves RNGH's own default in place instead of
470
+ // Impulse overwriting it with a guess.
471
+ if (hitSlop !== undefined) {
472
+ rotation.hitSlop(hitSlop)
473
+ }
474
+ if (enabled !== undefined) {
475
+ rotation.enabled(enabled)
476
+ }
477
+ return rotation
478
+ },
479
+ [
480
+ min,
481
+ max,
482
+ elastic,
483
+ hitSlop,
484
+ enabled,
485
+ hasRotateStart,
486
+ hasRotateEnd,
487
+ handleRotateStart,
488
+ handleRotateEnd,
489
+ angle,
490
+ anchor,
491
+ startAngle,
492
+ isActive,
493
+ onBegin,
494
+ onUpdate,
495
+ onFinalize,
496
+ ],
497
+ options,
498
+ )
499
+
500
+ // Memoised so a consumer can put the whole hook result in a dependency
501
+ // list. The shared values are stable for the life of the hook, so `built`
502
+ // is the only real input.
503
+ return useMemo(
504
+ () => buildIntentResult(built, { angle, anchor, isActive }),
505
+ [built, angle, anchor, isActive],
506
+ )
507
+ }