@rootnative/inertia-gestures 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.
package/llms.txt ADDED
@@ -0,0 +1,189 @@
1
+ > @rootnative/inertia-gestures — adapter package for @rootnative/inertia.
2
+ > This file is generated from the matching docs page by scripts/build-llms.mjs — do not edit by hand.
3
+
4
+ > Full docs: https://rootnative.github.io/inertia/
5
+ > Core overview: see @rootnative/inertia/llms.txt (or docs/static/llms.txt in the repo)
6
+ > Source: https://github.com/rootnative/inertia
7
+
8
+ ---
9
+ # Gestures adapter
10
+
11
+ `@rootnative/inertia-gestures` adds drag / swipe / pan hooks built on `react-native-gesture-handler`. It is an **optional** sibling package — install it only if you need richer-than-`pressable` gestures. The core library has no required `gesture-handler` dependency.
12
+
13
+ The hooks compose with any `Motion.*` primitive via a `<GestureDetector>` and a single `style` slot — they don't replace the `gesture` prop, they extend what's reachable beside it.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ yarn add @rootnative/inertia-gestures react-native-gesture-handler
19
+ ```
20
+
21
+ Then follow the [`react-native-gesture-handler` install guide](https://docs.swmansion.com/react-native-gesture-handler/docs/installation) — it needs `<GestureHandlerRootView>` near the root of your app.
22
+
23
+ ## `useDrag`
24
+
25
+ Drag a Motion primitive on one or two axes, with optional bounds and rubber-band overshoot.
26
+
27
+ ```tsx
28
+ import { GestureDetector } from 'react-native-gesture-handler'
29
+ import { Motion } from '@rootnative/inertia'
30
+ import { useDrag } from '@rootnative/inertia-gestures'
31
+
32
+ function DraggableBox() {
33
+ const drag = useDrag({
34
+ axis: 'both',
35
+ constraints: { left: -100, right: 100, top: -60, bottom: 60 },
36
+ elastic: 0.4,
37
+ })
38
+ return (
39
+ <GestureDetector gesture={drag.gesture}>
40
+ <Motion.View style={[styles.box, drag.animatedStyle]} />
41
+ </GestureDetector>
42
+ )
43
+ }
44
+ ```
45
+
46
+ | Option | Type | Default | Notes |
47
+ | ------------- | ---------------------------------------------- | -------- | --------------------------------------------------------------------------------------------------- |
48
+ | `axis` | `'x' \| 'y' \| 'both'` | `'both'` | Lock the gesture to one axis. |
49
+ | `constraints` | `{ left?, right?, top?, bottom? }` | none | Bounds in px from the resting position. Each side independently optional. |
50
+ | `elastic` | `number` (0–1) | `0` | Rubber-band coefficient past `constraints`. `0` hard-clamps; `0.4` is a typical Framer-Motion feel. |
51
+ | `onDragStart` | `() => void` | none | Fires on JS thread when drag begins. |
52
+ | `onDragEnd` | `(info: { x, y, velocity: { x, y } }) => void` | none | Fires on JS thread when drag ends. |
53
+ | `onRelease` | `(info) => ReleaseResult \| void` (worklet) | none | UI-thread release transition — see below. |
54
+
55
+ Returns `{ gesture, animatedStyle, dragX, dragY, isDragging }`. The shared values are exposed for power use cases (deriving secondary effects from drag position).
56
+
57
+ ### Release transitions (`onRelease`)
58
+
59
+ By default `useDrag` leaves the shared values exactly where the finger lifted. Pass `onRelease` to animate them to a settled position via Inertia's transition vocabulary — spring snap-to-tick, decay with bounds, timing settle. The callback runs as a **worklet** so the release velocity stays on the UI thread (no JS round-trip):
60
+
61
+ ```tsx
62
+ const drag = useDrag({
63
+ axis: 'x',
64
+ constraints: { left: 0, right: trackWidth },
65
+ onRelease: (e) => {
66
+ 'worklet'
67
+ // Snap to the nearest tick using the release velocity for natural continuation.
68
+ const snap = nearestTick(e.x, ticks)
69
+ return {
70
+ x: { type: 'spring', to: snap, velocity: e.velocity.x },
71
+ }
72
+ },
73
+ })
74
+ ```
75
+
76
+ Return per-axis release transitions in a `ReleaseResult` shape — omit an axis (or return nothing) to leave that SV where it landed. The transition shapes mirror the core `transition` prop:
77
+
78
+ ```ts
79
+ type ReleaseTransition =
80
+ | (SpringTransition & { to: number })
81
+ | (TimingTransition & { to: number })
82
+ | DecayTransition // no `to` — decay decelerates from current position
83
+ | (NoAnimationTransition & { to: number })
84
+ ```
85
+
86
+ Decay-on-release (for free-flick behaviour without snapping):
87
+
88
+ ```tsx
89
+ onRelease: (e) => {
90
+ 'worklet'
91
+ return {
92
+ x: { type: 'decay', velocity: e.velocity.x, clamp: [0, trackWidth] },
93
+ }
94
+ }
95
+ ```
96
+
97
+ `onRelease` composes additively with `onDragEnd` — both fire on release. Use `onRelease` for the SV animation; use `onDragEnd` for JS-thread side effects (analytics, state updates).
98
+
99
+ Internally this routes through `buildReleaseAnimation` exported from `@rootnative/inertia` — a worklet-safe builder that covers spring / timing / decay / no-animation single-step transitions. Reach for it directly if you're writing a custom `Gesture.Pan().onEnd(...)` worklet outside `useDrag`.
100
+
101
+ ## `useSwipe`
102
+
103
+ Directional commit-or-snap-back gesture. Tracks live translation while the user drags; on release, fires `onSwipe` if either the distance or velocity threshold is met along an allowed axis. Whether or not it commits, the position springs back to zero — the consumer drives the side effect (delete, dismiss, advance).
104
+
105
+ ```tsx
106
+ const swipe = useSwipe({
107
+ directions: ['left', 'right'],
108
+ distanceThreshold: 100,
109
+ onSwipe: (direction) => {
110
+ if (direction === 'left') dismiss()
111
+ if (direction === 'right') accept()
112
+ },
113
+ })
114
+
115
+ return (
116
+ <GestureDetector gesture={swipe.gesture}>
117
+ <Motion.View style={[styles.card, swipe.animatedStyle]}>
118
+ {children}
119
+ </Motion.View>
120
+ </GestureDetector>
121
+ )
122
+ ```
123
+
124
+ | Option | Type | Default | Notes |
125
+ | ------------------- | --------------------------------------------- | -------- | ----------------------------------------------------------------- |
126
+ | `directions` | `Array<'left' \| 'right' \| 'up' \| 'down'>` | all four | Only commits along listed directions. Off-axis swipes do nothing. |
127
+ | `distanceThreshold` | `number` (px) | `80` | Distance past which a release commits. |
128
+ | `velocityThreshold` | `number` (px/sec) | `800` | Flick velocity that commits even before distance threshold. |
129
+ | `onSwipe` | `(direction, { distance, velocity }) => void` | none | JS-thread callback. Direction is one of the allowed values. |
130
+
131
+ The dominant axis (whichever of \|tx\|, \|ty\| is larger at release) decides which direction is checked.
132
+
133
+ ## `usePan`
134
+
135
+ Camera-style pan with momentum on release. Position **persists across separate gestures** — the next pan starts from the current offset, not zero — and on release the position continues gliding via Reanimated's `withDecay`.
136
+
137
+ ```tsx
138
+ const pan = usePan({
139
+ constraints: { left: -240, right: 240, top: -240, bottom: 240 },
140
+ deceleration: 0.997,
141
+ })
142
+
143
+ return (
144
+ <GestureDetector gesture={pan.gesture}>
145
+ <Motion.View style={[styles.canvas, pan.animatedStyle]}>
146
+ {/* large content */}
147
+ </Motion.View>
148
+ </GestureDetector>
149
+ )
150
+ ```
151
+
152
+ | Option | Type | Default | Notes |
153
+ | ----------------- | ---------------------------------- | ------------------ | ----------------------------------------------------------------------------- |
154
+ | `constraints` | `{ left?, right?, top?, bottom? }` | none | Hard clamp during gesture and during decay. |
155
+ | `deceleration` | `number` | Reanimated default | Higher = momentum dies faster. Roughly `0.99` (slow) to `0.999` (long glide). |
156
+ | `disableMomentum` | `boolean` | `false` | Hard stop on release (drag-like). |
157
+
158
+ For rubber-banded constraints, prefer `useDrag({ elastic })` — `usePan` hard-clamps because it composes with `withDecay`'s `clamp` parameter, which doesn't have an overshoot mode.
159
+
160
+ ## When to pick which
161
+
162
+ | Gesture | Snap back on release? | Momentum on release? | Typical UX |
163
+ | ---------- | --------------------- | ---------------------------- | -------------------------------------------------- |
164
+ | `useDrag` | No — stays where left | No | Move-to-position, sliders, sortable handles. |
165
+ | `useSwipe` | Yes — always | No (springs back) | Swipe-to-delete rows, card stacks, dismiss sheets. |
166
+ | `usePan` | No — stays where left | Yes — coasts via `withDecay` | Maps, zoomable canvases, large-image navigation. |
167
+
168
+ ## Scope: touch + mouse only
169
+
170
+ All three hooks layer on `react-native-gesture-handler`, which surfaces touch and mouse events but **not keyboard**. For controls that need keyboard a11y in addition to drag — a Slider with arrow-key increment / decrement, a scrollbar with `PageUp` / `PageDown`, a range input — reach for [`useTouchDrag`](./api/hooks#usetouchdragoptions) from `@rootnative/inertia/touch` instead. It's a PanResponder-backed counterpart to `useDrag` that lives in core (no `react-native-gesture-handler` peer required) and composes cleanly with `onKeyDown` on the wrapping `Pressable`.
171
+
172
+ The full ladder:
173
+
174
+ 1. `Motion.View` + `gesture` prop — pure state-overlay UX (hover, focus, press).
175
+ 2. `useTouchDrag` from `@rootnative/inertia/touch` — pointer + keyboard drag, no extra peer dep.
176
+ 3. `useDrag` / `useSwipe` / `usePan` from this package — pointer-only, gesture-handler-backed. Best for projects already using gesture-handler; the UI-thread release path is more precise than PanResponder's JS-thread velocity.
177
+ 4. Raw `PanResponder` + `useAnimation` — drop here when the gesture lifecycle itself needs customizing (multi-touch composition, custom capture predicates beyond axis lock).
178
+
179
+ Both `useTouchDrag` and the gesture-handler `useDrag` accept the same `onRelease` shape and route through `buildReleaseAnimation`, so swapping between them only touches the wiring, not the release animation config.
180
+
181
+ ## Try it
182
+
183
+
184
+
185
+ ## Why a separate package?
186
+
187
+ The core library's `gesture` prop covers `pressed` / `focused` / `focusVisible` / `hovered` — boolean state-overlay gestures with no continuous translation. Drag, swipe, and pan are **value-driven**: the gesture publishes a continuous position that the consumer composes into the view's style. They don't fit the boolean-state model, and they need `react-native-gesture-handler` (which we don't want as a required peer of core).
188
+
189
+ Splitting them off keeps core's surface area minimal and lets the gesture-handler dependency stay opt-in. The hooks return primitives (`gesture`, `animatedStyle`, shared values) rather than wrapping Motion in a new component, so they layer cleanly on the existing `Motion.*` set.
package/package.json ADDED
@@ -0,0 +1,84 @@
1
+ {
2
+ "name": "@rootnative/inertia-gestures",
3
+ "version": "0.0.0-alpha.0",
4
+ "description": "Gesture-handler-driven drag / pan / swipe adapters for @rootnative/inertia.",
5
+ "license": "MIT",
6
+ "author": "RootNative",
7
+ "homepage": "https://github.com/rootnative/inertia",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/rootnative/inertia.git",
11
+ "directory": "packages/gestures"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/rootnative/inertia/issues"
15
+ },
16
+ "keywords": [
17
+ "react-native",
18
+ "reanimated",
19
+ "gesture-handler",
20
+ "drag",
21
+ "pan",
22
+ "swipe",
23
+ "inertia"
24
+ ],
25
+ "sideEffects": false,
26
+ "main": "./dist/index.js",
27
+ "module": "./dist/index.mjs",
28
+ "types": "./dist/index.d.ts",
29
+ "react-native": "./src/index.ts",
30
+ "source": "./src/index.ts",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "react-native": "./src/index.ts",
35
+ "source": "./src/index.ts",
36
+ "import": "./dist/index.mjs",
37
+ "require": "./dist/index.js"
38
+ },
39
+ "./package.json": "./package.json"
40
+ },
41
+ "files": [
42
+ "dist",
43
+ "src",
44
+ "llms.txt",
45
+ "README.md",
46
+ "LICENSE",
47
+ "CHANGELOG.md",
48
+ "!**/__tests__",
49
+ "!**/*.test.*"
50
+ ],
51
+ "peerDependencies": {
52
+ "@rootnative/inertia": ">=0.0.0-alpha.0",
53
+ "react": ">=19.0.0",
54
+ "react-native": ">=0.81.0",
55
+ "react-native-gesture-handler": ">=2.0.0",
56
+ "react-native-reanimated": ">=4.0.0"
57
+ },
58
+ "devDependencies": {
59
+ "@react-native/babel-preset": "^0.81.5",
60
+ "@testing-library/react-native": "^13.3.3",
61
+ "@types/jest": "^29.5.14",
62
+ "@types/react": "^19.1.0",
63
+ "jest": "^29.7.0",
64
+ "react": "19.1.0",
65
+ "react-native": "0.81.5",
66
+ "react-native-gesture-handler": "~2.28.0",
67
+ "react-native-reanimated": "~4.1.1",
68
+ "react-test-renderer": "19.1.0",
69
+ "tsup": "^8.3.5",
70
+ "typescript": "^5.7.3",
71
+ "@rootnative/inertia": "0.0.0-alpha.0"
72
+ },
73
+ "publishConfig": {
74
+ "access": "public"
75
+ },
76
+ "scripts": {
77
+ "build": "tsup",
78
+ "dev": "tsup --watch",
79
+ "typecheck": "tsc --noEmit",
80
+ "test": "jest",
81
+ "lint": "eslint .",
82
+ "clean": "rm -rf dist .turbo *.tsbuildinfo"
83
+ }
84
+ }
package/src/index.ts ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * `@rootnative/inertia-gestures` — gesture-handler-driven adapters for
3
+ * `@rootnative/inertia`.
4
+ *
5
+ * v0.2 surface:
6
+ * - `useDrag` — one- or two-axis drag with optional constraints and
7
+ * rubber-band elasticity.
8
+ * - `useSwipe` — directional commit-or-snap-back gesture (distance + velocity
9
+ * thresholds).
10
+ * - `usePan` — camera-style pan with momentum on release.
11
+ */
12
+ export { useDrag } from './useDrag'
13
+ export type { UseDragResult } from './useDrag'
14
+ export { useSwipe } from './useSwipe'
15
+ export type { SwipeDirection, SwipeOptions, UseSwipeResult } from './useSwipe'
16
+ export { usePan } from './usePan'
17
+ export type { PanOptions, UsePanResult } from './usePan'
18
+ export type {
19
+ DragConstraints,
20
+ DragOptions,
21
+ ReleaseInfo,
22
+ ReleaseResult,
23
+ ReleaseTransition,
24
+ } from './types'
package/src/types.ts ADDED
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Public types for `@rootnative/inertia-gestures`.
3
+ */
4
+
5
+ import type {
6
+ DecayTransition,
7
+ NoAnimationTransition,
8
+ SpringTransition,
9
+ TimingTransition,
10
+ } from '@rootnative/inertia'
11
+
12
+ /**
13
+ * Bounds the dragged value can reach. Each side is optional — omit to leave
14
+ * that direction unbounded. Coordinates are in the same space the drag
15
+ * publishes (pixels of translation from the dragged element's resting
16
+ * position), so `{ left: -100, right: 100 }` allows ±100 px of horizontal
17
+ * travel.
18
+ */
19
+ export interface DragConstraints {
20
+ left?: number
21
+ right?: number
22
+ top?: number
23
+ bottom?: number
24
+ }
25
+
26
+ /**
27
+ * UI-thread payload delivered to `onRelease` when the drag finishes. `x` and
28
+ * `y` are the final translations the SVs are sitting at; `velocity.x` /
29
+ * `velocity.y` are the release velocities in px/sec.
30
+ */
31
+ export interface ReleaseInfo {
32
+ x: number
33
+ y: number
34
+ velocity: { x: number; y: number }
35
+ }
36
+
37
+ /**
38
+ * Per-axis release transition. Spring / timing / no-animation animate to a
39
+ * target value (`to`); decay decelerates from the current position via its
40
+ * own physics and has no `to`. Pass the release velocity from `ReleaseInfo`
41
+ * into the transition's own `velocity` field for natural continuation.
42
+ */
43
+ export type ReleaseTransition =
44
+ | (SpringTransition & { to: number })
45
+ | (TimingTransition & { to: number })
46
+ | DecayTransition
47
+ | (NoAnimationTransition & { to: number })
48
+
49
+ /**
50
+ * Per-axis release transitions returned by `onRelease`. Omit an axis to leave
51
+ * its SV where it landed (no release animation on that axis).
52
+ */
53
+ export interface ReleaseResult {
54
+ x?: ReleaseTransition
55
+ y?: ReleaseTransition
56
+ }
57
+
58
+ /**
59
+ * Configuration for `useDrag`. All fields are optional; the defaults give an
60
+ * unconstrained two-axis drag with no elasticity.
61
+ */
62
+ export interface DragOptions {
63
+ /**
64
+ * Restrict the drag to one axis. Defaults to `'both'`. When `'x'` is set
65
+ * the y-axis shared value never updates (and vice versa); the gesture
66
+ * still tracks both for velocity reporting on `onDragEnd`.
67
+ */
68
+ axis?: 'x' | 'y' | 'both'
69
+ /**
70
+ * Travel bounds. Out-of-bounds values clamp to the limit unless `elastic`
71
+ * is non-zero, in which case overshoot is dampened (rubber-band feel).
72
+ */
73
+ constraints?: DragConstraints
74
+ /**
75
+ * Rubber-band coefficient applied to overshoot past `constraints`. `0`
76
+ * (default) hard-clamps; `1` is fully elastic (no resistance). Typical
77
+ * Framer-Motion-style feel sits around `0.2`–`0.4`.
78
+ */
79
+ elastic?: number
80
+ /**
81
+ * Fired on the JS thread when the drag begins.
82
+ */
83
+ onDragStart?: () => void
84
+ /**
85
+ * Fired on the JS thread when the drag finishes (release or cancel). The
86
+ * payload is the final translation and the release velocity in px/sec.
87
+ */
88
+ onDragEnd?: (info: ReleaseInfo) => void
89
+ /**
90
+ * UI-thread callback fired on release. Return per-axis release transitions
91
+ * to animate the SVs to a settled position via Inertia's transition
92
+ * resolver — spring snap-to-tick, decay with bounds, timing settle, etc.
93
+ * Omit an axis (or return nothing) to leave that SV where it landed.
94
+ *
95
+ * This callback runs as a worklet so the release velocity stays on the UI
96
+ * thread. Author it with the `'worklet'` directive at the top of the body.
97
+ *
98
+ * Composes with `onDragEnd`: both fire on release. `onRelease` controls SV
99
+ * animation on the UI thread; `onDragEnd` is for JS-thread side effects
100
+ * (analytics, state updates).
101
+ */
102
+ onRelease?: (info: ReleaseInfo) => ReleaseResult | void
103
+ }
package/src/useDrag.ts ADDED
@@ -0,0 +1,184 @@
1
+ import { useMemo } from 'react'
2
+ import { Gesture, type PanGesture } from 'react-native-gesture-handler'
3
+ import {
4
+ runOnJS,
5
+ useAnimatedStyle,
6
+ useSharedValue,
7
+ type SharedValue,
8
+ } from 'react-native-reanimated'
9
+ import { buildReleaseAnimation } from '@rootnative/inertia'
10
+ import type { DragConstraints, DragOptions } from './types'
11
+
12
+ export interface UseDragResult {
13
+ /** Pan gesture to pass to a `<GestureDetector>`. */
14
+ gesture: PanGesture
15
+ /**
16
+ * Animated style fragment (a single `transform` entry) to stack onto the
17
+ * dragged Motion primitive's `style` prop. Stable across renders.
18
+ */
19
+ animatedStyle: ReturnType<typeof useAnimatedStyle>
20
+ /** Current x translation in pixels. UI-thread shared value. */
21
+ dragX: SharedValue<number>
22
+ /** Current y translation in pixels. UI-thread shared value. */
23
+ dragY: SharedValue<number>
24
+ /** True while the gesture is active. */
25
+ isDragging: SharedValue<boolean>
26
+ }
27
+
28
+ /**
29
+ * Drag a Motion primitive with `react-native-gesture-handler`'s pan gesture.
30
+ *
31
+ * The hook owns a pair of shared values (`dragX`, `dragY`) and a `Pan`
32
+ * gesture that updates them on the UI thread. The returned `animatedStyle`
33
+ * is a self-contained `transform: [{ translateX }, { translateY }]` fragment;
34
+ * stack it onto the dragged component without colliding with Motion's own
35
+ * `animate` transforms.
36
+ *
37
+ * Usage:
38
+ * ```tsx
39
+ * const drag = useDrag({ axis: 'x', constraints: { left: -100, right: 100 } })
40
+ * return (
41
+ * <GestureDetector gesture={drag.gesture}>
42
+ * <Motion.View style={drag.animatedStyle} />
43
+ * </GestureDetector>
44
+ * )
45
+ * ```
46
+ */
47
+ export function useDrag(options: DragOptions = {}): UseDragResult {
48
+ const {
49
+ axis = 'both',
50
+ constraints,
51
+ elastic = 0,
52
+ onDragStart,
53
+ onDragEnd,
54
+ onRelease,
55
+ } = options
56
+
57
+ const dragX = useSharedValue(0)
58
+ const dragY = useSharedValue(0)
59
+ const startX = useSharedValue(0)
60
+ const startY = useSharedValue(0)
61
+ const isDragging = useSharedValue(false)
62
+
63
+ // Snapshot scalars into local consts so the Pan handlers (worklets) capture
64
+ // primitives, not the closing `options` object — a fresh `options` literal
65
+ // each render would otherwise force a new gesture identity.
66
+ const lockX = axis !== 'y'
67
+ const lockY = axis !== 'x'
68
+ const left = constraints?.left
69
+ const right = constraints?.right
70
+ const top = constraints?.top
71
+ const bottom = constraints?.bottom
72
+ const elasticCoef = elastic
73
+
74
+ const gesture = useMemo(() => {
75
+ const pan = Gesture.Pan()
76
+ .onStart(() => {
77
+ 'worklet'
78
+ startX.value = dragX.value
79
+ startY.value = dragY.value
80
+ isDragging.value = true
81
+ if (onDragStart) runOnJS(onDragStart)()
82
+ })
83
+ .onUpdate((e) => {
84
+ 'worklet'
85
+ if (lockX) {
86
+ dragX.value = applyBounds(
87
+ startX.value + e.translationX,
88
+ left,
89
+ right,
90
+ elasticCoef,
91
+ )
92
+ }
93
+ if (lockY) {
94
+ dragY.value = applyBounds(
95
+ startY.value + e.translationY,
96
+ top,
97
+ bottom,
98
+ elasticCoef,
99
+ )
100
+ }
101
+ })
102
+ .onEnd((e) => {
103
+ 'worklet'
104
+ isDragging.value = false
105
+ const x = dragX.value
106
+ const y = dragY.value
107
+ const vx = e.velocityX
108
+ const vy = e.velocityY
109
+ if (onRelease) {
110
+ const result = onRelease({ x, y, velocity: { x: vx, y: vy } })
111
+ if (result) {
112
+ // Decay ignores `toValue` (it has no target); spring/timing/no-animation
113
+ // use it. Build per axis off the SV's current position when no `to`
114
+ // is given (decay branch), the destination otherwise.
115
+ if (result.x && lockX) {
116
+ const toX = 'to' in result.x ? result.x.to : x
117
+ dragX.value = buildReleaseAnimation(
118
+ result.x,
119
+ toX,
120
+ ) as unknown as number
121
+ }
122
+ if (result.y && lockY) {
123
+ const toY = 'to' in result.y ? result.y.to : y
124
+ dragY.value = buildReleaseAnimation(
125
+ result.y,
126
+ toY,
127
+ ) as unknown as number
128
+ }
129
+ }
130
+ }
131
+ if (onDragEnd) {
132
+ runOnJS(onDragEnd)({ x, y, velocity: { x: vx, y: vy } })
133
+ }
134
+ })
135
+ return pan
136
+ }, [
137
+ lockX,
138
+ lockY,
139
+ left,
140
+ right,
141
+ top,
142
+ bottom,
143
+ elasticCoef,
144
+ onDragStart,
145
+ onDragEnd,
146
+ onRelease,
147
+ dragX,
148
+ dragY,
149
+ startX,
150
+ startY,
151
+ isDragging,
152
+ ])
153
+
154
+ const animatedStyle = useAnimatedStyle(() => ({
155
+ transform: [{ translateX: dragX.value }, { translateY: dragY.value }],
156
+ }))
157
+
158
+ return { gesture, animatedStyle, dragX, dragY, isDragging }
159
+ }
160
+
161
+ /**
162
+ * Clamp `value` to `[min, max]`. When `elastic > 0` the overshoot beyond a
163
+ * bound is scaled by `elastic` instead of hard-clamped, giving a rubber-band
164
+ * feel. `min` / `max` may be `undefined` to leave that side unbounded.
165
+ *
166
+ * Worklet — runs on the UI thread inside the pan handler.
167
+ */
168
+ function applyBounds(
169
+ value: number,
170
+ min: number | undefined,
171
+ max: number | undefined,
172
+ elastic: number,
173
+ ): number {
174
+ 'worklet'
175
+ if (min !== undefined && value < min) {
176
+ return elastic > 0 ? min + (value - min) * elastic : min
177
+ }
178
+ if (max !== undefined && value > max) {
179
+ return elastic > 0 ? max + (value - max) * elastic : max
180
+ }
181
+ return value
182
+ }
183
+
184
+ export type { DragConstraints, DragOptions }