@onlynative/inertia-gestures 0.0.1-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.
- package/LICENSE +21 -0
- package/dist/index.d.mts +207 -0
- package/dist/index.d.ts +207 -0
- package/dist/index.js +259 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +255 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +81 -0
- package/src/index.ts +18 -0
- package/src/types.ts +54 -0
- package/src/useDrag.ts +159 -0
- package/src/usePan.ts +164 -0
- package/src/useSwipe.ts +195 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 OnlyNative
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { PanGesture } from 'react-native-gesture-handler';
|
|
2
|
+
import { useAnimatedStyle, SharedValue } from 'react-native-reanimated';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Public types for `@onlynative/inertia-gestures`.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Bounds the dragged value can reach. Each side is optional — omit to leave
|
|
9
|
+
* that direction unbounded. Coordinates are in the same space the drag
|
|
10
|
+
* publishes (pixels of translation from the dragged element's resting
|
|
11
|
+
* position), so `{ left: -100, right: 100 }` allows ±100 px of horizontal
|
|
12
|
+
* travel.
|
|
13
|
+
*/
|
|
14
|
+
interface DragConstraints {
|
|
15
|
+
left?: number;
|
|
16
|
+
right?: number;
|
|
17
|
+
top?: number;
|
|
18
|
+
bottom?: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Configuration for `useDrag`. All fields are optional; the defaults give an
|
|
22
|
+
* unconstrained two-axis drag with no elasticity.
|
|
23
|
+
*/
|
|
24
|
+
interface DragOptions {
|
|
25
|
+
/**
|
|
26
|
+
* Restrict the drag to one axis. Defaults to `'both'`. When `'x'` is set
|
|
27
|
+
* the y-axis shared value never updates (and vice versa); the gesture
|
|
28
|
+
* still tracks both for velocity reporting on `onDragEnd`.
|
|
29
|
+
*/
|
|
30
|
+
axis?: 'x' | 'y' | 'both';
|
|
31
|
+
/**
|
|
32
|
+
* Travel bounds. Out-of-bounds values clamp to the limit unless `elastic`
|
|
33
|
+
* is non-zero, in which case overshoot is dampened (rubber-band feel).
|
|
34
|
+
*/
|
|
35
|
+
constraints?: DragConstraints;
|
|
36
|
+
/**
|
|
37
|
+
* Rubber-band coefficient applied to overshoot past `constraints`. `0`
|
|
38
|
+
* (default) hard-clamps; `1` is fully elastic (no resistance). Typical
|
|
39
|
+
* Framer-Motion-style feel sits around `0.2`–`0.4`.
|
|
40
|
+
*/
|
|
41
|
+
elastic?: number;
|
|
42
|
+
/**
|
|
43
|
+
* Fired on the JS thread when the drag begins.
|
|
44
|
+
*/
|
|
45
|
+
onDragStart?: () => void;
|
|
46
|
+
/**
|
|
47
|
+
* Fired on the JS thread when the drag finishes (release or cancel). The
|
|
48
|
+
* payload is the final translation and the release velocity in px/sec.
|
|
49
|
+
*/
|
|
50
|
+
onDragEnd?: (info: {
|
|
51
|
+
x: number;
|
|
52
|
+
y: number;
|
|
53
|
+
velocity: {
|
|
54
|
+
x: number;
|
|
55
|
+
y: number;
|
|
56
|
+
};
|
|
57
|
+
}) => void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface UseDragResult {
|
|
61
|
+
/** Pan gesture to pass to a `<GestureDetector>`. */
|
|
62
|
+
gesture: PanGesture;
|
|
63
|
+
/**
|
|
64
|
+
* Animated style fragment (a single `transform` entry) to stack onto the
|
|
65
|
+
* dragged Motion primitive's `style` prop. Stable across renders.
|
|
66
|
+
*/
|
|
67
|
+
animatedStyle: ReturnType<typeof useAnimatedStyle>;
|
|
68
|
+
/** Current x translation in pixels. UI-thread shared value. */
|
|
69
|
+
dragX: SharedValue<number>;
|
|
70
|
+
/** Current y translation in pixels. UI-thread shared value. */
|
|
71
|
+
dragY: SharedValue<number>;
|
|
72
|
+
/** True while the gesture is active. */
|
|
73
|
+
isDragging: SharedValue<boolean>;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Drag a Motion primitive with `react-native-gesture-handler`'s pan gesture.
|
|
77
|
+
*
|
|
78
|
+
* The hook owns a pair of shared values (`dragX`, `dragY`) and a `Pan`
|
|
79
|
+
* gesture that updates them on the UI thread. The returned `animatedStyle`
|
|
80
|
+
* is a self-contained `transform: [{ translateX }, { translateY }]` fragment;
|
|
81
|
+
* stack it onto the dragged component without colliding with Motion's own
|
|
82
|
+
* `animate` transforms.
|
|
83
|
+
*
|
|
84
|
+
* Usage:
|
|
85
|
+
* ```tsx
|
|
86
|
+
* const drag = useDrag({ axis: 'x', constraints: { left: -100, right: 100 } })
|
|
87
|
+
* return (
|
|
88
|
+
* <GestureDetector gesture={drag.gesture}>
|
|
89
|
+
* <Motion.View style={drag.animatedStyle} />
|
|
90
|
+
* </GestureDetector>
|
|
91
|
+
* )
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
declare function useDrag(options?: DragOptions): UseDragResult;
|
|
95
|
+
|
|
96
|
+
type SwipeDirection = 'left' | 'right' | 'up' | 'down';
|
|
97
|
+
interface SwipeOptions {
|
|
98
|
+
/**
|
|
99
|
+
* Allowed swipe directions. Defaults to all four. The gesture only commits
|
|
100
|
+
* for directions in this list — a horizontal swipe with `directions:
|
|
101
|
+
* ['up', 'down']` will not fire `onSwipe`.
|
|
102
|
+
*/
|
|
103
|
+
directions?: SwipeDirection[];
|
|
104
|
+
/**
|
|
105
|
+
* Pixel distance threshold past which a release commits the swipe. Defaults
|
|
106
|
+
* to `80`.
|
|
107
|
+
*/
|
|
108
|
+
distanceThreshold?: number;
|
|
109
|
+
/**
|
|
110
|
+
* Velocity threshold (px/sec) past which a release commits the swipe even
|
|
111
|
+
* before the distance threshold is reached — flick-style gestures. Defaults
|
|
112
|
+
* to `800`.
|
|
113
|
+
*/
|
|
114
|
+
velocityThreshold?: number;
|
|
115
|
+
/**
|
|
116
|
+
* Fired on the JS thread when the gesture commits in an allowed direction.
|
|
117
|
+
*/
|
|
118
|
+
onSwipe?: (direction: SwipeDirection, info: {
|
|
119
|
+
distance: number;
|
|
120
|
+
velocity: number;
|
|
121
|
+
}) => void;
|
|
122
|
+
}
|
|
123
|
+
interface UseSwipeResult {
|
|
124
|
+
/** Pan gesture to pass to a `<GestureDetector>`. */
|
|
125
|
+
gesture: PanGesture;
|
|
126
|
+
/**
|
|
127
|
+
* Animated style fragment exposing live translation while the gesture is
|
|
128
|
+
* active. Snaps back to `{ 0, 0 }` after release (whether or not the swipe
|
|
129
|
+
* committed) via a default spring.
|
|
130
|
+
*/
|
|
131
|
+
animatedStyle: ReturnType<typeof useAnimatedStyle>;
|
|
132
|
+
/** Live x translation. */
|
|
133
|
+
swipeX: SharedValue<number>;
|
|
134
|
+
/** Live y translation. */
|
|
135
|
+
swipeY: SharedValue<number>;
|
|
136
|
+
/** True while the user is actively swiping. */
|
|
137
|
+
isActive: SharedValue<boolean>;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Directional commit-or-snap-back gesture. Tracks live translation while the
|
|
141
|
+
* user drags and fires `onSwipe(direction)` on release if either the distance
|
|
142
|
+
* or velocity threshold is exceeded in an allowed direction. The position
|
|
143
|
+
* shared values always animate back to zero — the consumer is responsible
|
|
144
|
+
* for whatever side effect the commit drives (delete a row, dismiss a sheet,
|
|
145
|
+
* etc.).
|
|
146
|
+
*
|
|
147
|
+
* Usage:
|
|
148
|
+
* ```tsx
|
|
149
|
+
* const swipe = useSwipe({
|
|
150
|
+
* directions: ['left'],
|
|
151
|
+
* onSwipe: (dir) => deleteRow(),
|
|
152
|
+
* })
|
|
153
|
+
* return (
|
|
154
|
+
* <GestureDetector gesture={swipe.gesture}>
|
|
155
|
+
* <Motion.View style={swipe.animatedStyle}>...</Motion.View>
|
|
156
|
+
* </GestureDetector>
|
|
157
|
+
* )
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
160
|
+
declare function useSwipe(options?: SwipeOptions): UseSwipeResult;
|
|
161
|
+
|
|
162
|
+
interface PanOptions {
|
|
163
|
+
/**
|
|
164
|
+
* Translation bounds. Each side is optional; out-of-bounds motion during
|
|
165
|
+
* the active gesture and during the post-release decay is hard-clamped
|
|
166
|
+
* (Reanimated's `withDecay` `clamp` param). Decay-style overshoot is not
|
|
167
|
+
* supported here — for rubber-banded bounds, prefer `useDrag` with
|
|
168
|
+
* `elastic`.
|
|
169
|
+
*/
|
|
170
|
+
constraints?: DragConstraints;
|
|
171
|
+
/**
|
|
172
|
+
* Deceleration applied to the post-release momentum. Higher = momentum
|
|
173
|
+
* dies faster. Reanimated default is `0.998`; lower values feel more
|
|
174
|
+
* "slippy". Range: roughly `0.99` (slow) to `0.999` (long glide).
|
|
175
|
+
*/
|
|
176
|
+
deceleration?: number;
|
|
177
|
+
/**
|
|
178
|
+
* Disable the post-release momentum entirely. Defaults to `false` — pan
|
|
179
|
+
* coasts after release. Set to `true` for a hard stop on release (drag-like
|
|
180
|
+
* behavior).
|
|
181
|
+
*/
|
|
182
|
+
disableMomentum?: boolean;
|
|
183
|
+
}
|
|
184
|
+
interface UsePanResult {
|
|
185
|
+
/** Pan gesture to pass to a `<GestureDetector>`. */
|
|
186
|
+
gesture: PanGesture;
|
|
187
|
+
/** Stable animated `transform` style. */
|
|
188
|
+
animatedStyle: ReturnType<typeof useAnimatedStyle>;
|
|
189
|
+
/** Live x translation, persistent across gestures. */
|
|
190
|
+
panX: SharedValue<number>;
|
|
191
|
+
/** Live y translation, persistent across gestures. */
|
|
192
|
+
panY: SharedValue<number>;
|
|
193
|
+
/** True while the user is actively panning. Decay phase reads `false`. */
|
|
194
|
+
isPanning: SharedValue<boolean>;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Camera-pan-style drag with momentum on release. Translation persists
|
|
198
|
+
* across separate pan gestures (the next pan starts from the current
|
|
199
|
+
* position, not zero), and on release the translation continues to glide
|
|
200
|
+
* via Reanimated's `withDecay` until friction stops it.
|
|
201
|
+
*
|
|
202
|
+
* Use for map / zoom-canvas / large-image navigation. For dragging an
|
|
203
|
+
* element to a position with no momentum, use `useDrag` instead.
|
|
204
|
+
*/
|
|
205
|
+
declare function usePan(options?: PanOptions): UsePanResult;
|
|
206
|
+
|
|
207
|
+
export { type DragConstraints, type DragOptions, type PanOptions, type SwipeDirection, type SwipeOptions, type UseDragResult, type UsePanResult, type UseSwipeResult, useDrag, usePan, useSwipe };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { PanGesture } from 'react-native-gesture-handler';
|
|
2
|
+
import { useAnimatedStyle, SharedValue } from 'react-native-reanimated';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Public types for `@onlynative/inertia-gestures`.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Bounds the dragged value can reach. Each side is optional — omit to leave
|
|
9
|
+
* that direction unbounded. Coordinates are in the same space the drag
|
|
10
|
+
* publishes (pixels of translation from the dragged element's resting
|
|
11
|
+
* position), so `{ left: -100, right: 100 }` allows ±100 px of horizontal
|
|
12
|
+
* travel.
|
|
13
|
+
*/
|
|
14
|
+
interface DragConstraints {
|
|
15
|
+
left?: number;
|
|
16
|
+
right?: number;
|
|
17
|
+
top?: number;
|
|
18
|
+
bottom?: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Configuration for `useDrag`. All fields are optional; the defaults give an
|
|
22
|
+
* unconstrained two-axis drag with no elasticity.
|
|
23
|
+
*/
|
|
24
|
+
interface DragOptions {
|
|
25
|
+
/**
|
|
26
|
+
* Restrict the drag to one axis. Defaults to `'both'`. When `'x'` is set
|
|
27
|
+
* the y-axis shared value never updates (and vice versa); the gesture
|
|
28
|
+
* still tracks both for velocity reporting on `onDragEnd`.
|
|
29
|
+
*/
|
|
30
|
+
axis?: 'x' | 'y' | 'both';
|
|
31
|
+
/**
|
|
32
|
+
* Travel bounds. Out-of-bounds values clamp to the limit unless `elastic`
|
|
33
|
+
* is non-zero, in which case overshoot is dampened (rubber-band feel).
|
|
34
|
+
*/
|
|
35
|
+
constraints?: DragConstraints;
|
|
36
|
+
/**
|
|
37
|
+
* Rubber-band coefficient applied to overshoot past `constraints`. `0`
|
|
38
|
+
* (default) hard-clamps; `1` is fully elastic (no resistance). Typical
|
|
39
|
+
* Framer-Motion-style feel sits around `0.2`–`0.4`.
|
|
40
|
+
*/
|
|
41
|
+
elastic?: number;
|
|
42
|
+
/**
|
|
43
|
+
* Fired on the JS thread when the drag begins.
|
|
44
|
+
*/
|
|
45
|
+
onDragStart?: () => void;
|
|
46
|
+
/**
|
|
47
|
+
* Fired on the JS thread when the drag finishes (release or cancel). The
|
|
48
|
+
* payload is the final translation and the release velocity in px/sec.
|
|
49
|
+
*/
|
|
50
|
+
onDragEnd?: (info: {
|
|
51
|
+
x: number;
|
|
52
|
+
y: number;
|
|
53
|
+
velocity: {
|
|
54
|
+
x: number;
|
|
55
|
+
y: number;
|
|
56
|
+
};
|
|
57
|
+
}) => void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
interface UseDragResult {
|
|
61
|
+
/** Pan gesture to pass to a `<GestureDetector>`. */
|
|
62
|
+
gesture: PanGesture;
|
|
63
|
+
/**
|
|
64
|
+
* Animated style fragment (a single `transform` entry) to stack onto the
|
|
65
|
+
* dragged Motion primitive's `style` prop. Stable across renders.
|
|
66
|
+
*/
|
|
67
|
+
animatedStyle: ReturnType<typeof useAnimatedStyle>;
|
|
68
|
+
/** Current x translation in pixels. UI-thread shared value. */
|
|
69
|
+
dragX: SharedValue<number>;
|
|
70
|
+
/** Current y translation in pixels. UI-thread shared value. */
|
|
71
|
+
dragY: SharedValue<number>;
|
|
72
|
+
/** True while the gesture is active. */
|
|
73
|
+
isDragging: SharedValue<boolean>;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Drag a Motion primitive with `react-native-gesture-handler`'s pan gesture.
|
|
77
|
+
*
|
|
78
|
+
* The hook owns a pair of shared values (`dragX`, `dragY`) and a `Pan`
|
|
79
|
+
* gesture that updates them on the UI thread. The returned `animatedStyle`
|
|
80
|
+
* is a self-contained `transform: [{ translateX }, { translateY }]` fragment;
|
|
81
|
+
* stack it onto the dragged component without colliding with Motion's own
|
|
82
|
+
* `animate` transforms.
|
|
83
|
+
*
|
|
84
|
+
* Usage:
|
|
85
|
+
* ```tsx
|
|
86
|
+
* const drag = useDrag({ axis: 'x', constraints: { left: -100, right: 100 } })
|
|
87
|
+
* return (
|
|
88
|
+
* <GestureDetector gesture={drag.gesture}>
|
|
89
|
+
* <Motion.View style={drag.animatedStyle} />
|
|
90
|
+
* </GestureDetector>
|
|
91
|
+
* )
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
declare function useDrag(options?: DragOptions): UseDragResult;
|
|
95
|
+
|
|
96
|
+
type SwipeDirection = 'left' | 'right' | 'up' | 'down';
|
|
97
|
+
interface SwipeOptions {
|
|
98
|
+
/**
|
|
99
|
+
* Allowed swipe directions. Defaults to all four. The gesture only commits
|
|
100
|
+
* for directions in this list — a horizontal swipe with `directions:
|
|
101
|
+
* ['up', 'down']` will not fire `onSwipe`.
|
|
102
|
+
*/
|
|
103
|
+
directions?: SwipeDirection[];
|
|
104
|
+
/**
|
|
105
|
+
* Pixel distance threshold past which a release commits the swipe. Defaults
|
|
106
|
+
* to `80`.
|
|
107
|
+
*/
|
|
108
|
+
distanceThreshold?: number;
|
|
109
|
+
/**
|
|
110
|
+
* Velocity threshold (px/sec) past which a release commits the swipe even
|
|
111
|
+
* before the distance threshold is reached — flick-style gestures. Defaults
|
|
112
|
+
* to `800`.
|
|
113
|
+
*/
|
|
114
|
+
velocityThreshold?: number;
|
|
115
|
+
/**
|
|
116
|
+
* Fired on the JS thread when the gesture commits in an allowed direction.
|
|
117
|
+
*/
|
|
118
|
+
onSwipe?: (direction: SwipeDirection, info: {
|
|
119
|
+
distance: number;
|
|
120
|
+
velocity: number;
|
|
121
|
+
}) => void;
|
|
122
|
+
}
|
|
123
|
+
interface UseSwipeResult {
|
|
124
|
+
/** Pan gesture to pass to a `<GestureDetector>`. */
|
|
125
|
+
gesture: PanGesture;
|
|
126
|
+
/**
|
|
127
|
+
* Animated style fragment exposing live translation while the gesture is
|
|
128
|
+
* active. Snaps back to `{ 0, 0 }` after release (whether or not the swipe
|
|
129
|
+
* committed) via a default spring.
|
|
130
|
+
*/
|
|
131
|
+
animatedStyle: ReturnType<typeof useAnimatedStyle>;
|
|
132
|
+
/** Live x translation. */
|
|
133
|
+
swipeX: SharedValue<number>;
|
|
134
|
+
/** Live y translation. */
|
|
135
|
+
swipeY: SharedValue<number>;
|
|
136
|
+
/** True while the user is actively swiping. */
|
|
137
|
+
isActive: SharedValue<boolean>;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Directional commit-or-snap-back gesture. Tracks live translation while the
|
|
141
|
+
* user drags and fires `onSwipe(direction)` on release if either the distance
|
|
142
|
+
* or velocity threshold is exceeded in an allowed direction. The position
|
|
143
|
+
* shared values always animate back to zero — the consumer is responsible
|
|
144
|
+
* for whatever side effect the commit drives (delete a row, dismiss a sheet,
|
|
145
|
+
* etc.).
|
|
146
|
+
*
|
|
147
|
+
* Usage:
|
|
148
|
+
* ```tsx
|
|
149
|
+
* const swipe = useSwipe({
|
|
150
|
+
* directions: ['left'],
|
|
151
|
+
* onSwipe: (dir) => deleteRow(),
|
|
152
|
+
* })
|
|
153
|
+
* return (
|
|
154
|
+
* <GestureDetector gesture={swipe.gesture}>
|
|
155
|
+
* <Motion.View style={swipe.animatedStyle}>...</Motion.View>
|
|
156
|
+
* </GestureDetector>
|
|
157
|
+
* )
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
160
|
+
declare function useSwipe(options?: SwipeOptions): UseSwipeResult;
|
|
161
|
+
|
|
162
|
+
interface PanOptions {
|
|
163
|
+
/**
|
|
164
|
+
* Translation bounds. Each side is optional; out-of-bounds motion during
|
|
165
|
+
* the active gesture and during the post-release decay is hard-clamped
|
|
166
|
+
* (Reanimated's `withDecay` `clamp` param). Decay-style overshoot is not
|
|
167
|
+
* supported here — for rubber-banded bounds, prefer `useDrag` with
|
|
168
|
+
* `elastic`.
|
|
169
|
+
*/
|
|
170
|
+
constraints?: DragConstraints;
|
|
171
|
+
/**
|
|
172
|
+
* Deceleration applied to the post-release momentum. Higher = momentum
|
|
173
|
+
* dies faster. Reanimated default is `0.998`; lower values feel more
|
|
174
|
+
* "slippy". Range: roughly `0.99` (slow) to `0.999` (long glide).
|
|
175
|
+
*/
|
|
176
|
+
deceleration?: number;
|
|
177
|
+
/**
|
|
178
|
+
* Disable the post-release momentum entirely. Defaults to `false` — pan
|
|
179
|
+
* coasts after release. Set to `true` for a hard stop on release (drag-like
|
|
180
|
+
* behavior).
|
|
181
|
+
*/
|
|
182
|
+
disableMomentum?: boolean;
|
|
183
|
+
}
|
|
184
|
+
interface UsePanResult {
|
|
185
|
+
/** Pan gesture to pass to a `<GestureDetector>`. */
|
|
186
|
+
gesture: PanGesture;
|
|
187
|
+
/** Stable animated `transform` style. */
|
|
188
|
+
animatedStyle: ReturnType<typeof useAnimatedStyle>;
|
|
189
|
+
/** Live x translation, persistent across gestures. */
|
|
190
|
+
panX: SharedValue<number>;
|
|
191
|
+
/** Live y translation, persistent across gestures. */
|
|
192
|
+
panY: SharedValue<number>;
|
|
193
|
+
/** True while the user is actively panning. Decay phase reads `false`. */
|
|
194
|
+
isPanning: SharedValue<boolean>;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Camera-pan-style drag with momentum on release. Translation persists
|
|
198
|
+
* across separate pan gestures (the next pan starts from the current
|
|
199
|
+
* position, not zero), and on release the translation continues to glide
|
|
200
|
+
* via Reanimated's `withDecay` until friction stops it.
|
|
201
|
+
*
|
|
202
|
+
* Use for map / zoom-canvas / large-image navigation. For dragging an
|
|
203
|
+
* element to a position with no momentum, use `useDrag` instead.
|
|
204
|
+
*/
|
|
205
|
+
declare function usePan(options?: PanOptions): UsePanResult;
|
|
206
|
+
|
|
207
|
+
export { type DragConstraints, type DragOptions, type PanOptions, type SwipeDirection, type SwipeOptions, type UseDragResult, type UsePanResult, type UseSwipeResult, useDrag, usePan, useSwipe };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var reactNativeGestureHandler = require('react-native-gesture-handler');
|
|
5
|
+
var reactNativeReanimated = require('react-native-reanimated');
|
|
6
|
+
|
|
7
|
+
// src/useDrag.ts
|
|
8
|
+
function useDrag(options = {}) {
|
|
9
|
+
const {
|
|
10
|
+
axis = "both",
|
|
11
|
+
constraints,
|
|
12
|
+
elastic = 0,
|
|
13
|
+
onDragStart,
|
|
14
|
+
onDragEnd
|
|
15
|
+
} = options;
|
|
16
|
+
const dragX = reactNativeReanimated.useSharedValue(0);
|
|
17
|
+
const dragY = reactNativeReanimated.useSharedValue(0);
|
|
18
|
+
const startX = reactNativeReanimated.useSharedValue(0);
|
|
19
|
+
const startY = reactNativeReanimated.useSharedValue(0);
|
|
20
|
+
const isDragging = reactNativeReanimated.useSharedValue(false);
|
|
21
|
+
const lockX = axis !== "y";
|
|
22
|
+
const lockY = axis !== "x";
|
|
23
|
+
const left = constraints?.left;
|
|
24
|
+
const right = constraints?.right;
|
|
25
|
+
const top = constraints?.top;
|
|
26
|
+
const bottom = constraints?.bottom;
|
|
27
|
+
const elasticCoef = elastic;
|
|
28
|
+
const gesture = react.useMemo(() => {
|
|
29
|
+
const pan = reactNativeGestureHandler.Gesture.Pan().onStart(() => {
|
|
30
|
+
"worklet";
|
|
31
|
+
startX.value = dragX.value;
|
|
32
|
+
startY.value = dragY.value;
|
|
33
|
+
isDragging.value = true;
|
|
34
|
+
if (onDragStart) reactNativeReanimated.runOnJS(onDragStart)();
|
|
35
|
+
}).onUpdate((e) => {
|
|
36
|
+
"worklet";
|
|
37
|
+
if (lockX) {
|
|
38
|
+
dragX.value = applyBounds(
|
|
39
|
+
startX.value + e.translationX,
|
|
40
|
+
left,
|
|
41
|
+
right,
|
|
42
|
+
elasticCoef
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
if (lockY) {
|
|
46
|
+
dragY.value = applyBounds(
|
|
47
|
+
startY.value + e.translationY,
|
|
48
|
+
top,
|
|
49
|
+
bottom,
|
|
50
|
+
elasticCoef
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}).onEnd((e) => {
|
|
54
|
+
"worklet";
|
|
55
|
+
isDragging.value = false;
|
|
56
|
+
if (onDragEnd) {
|
|
57
|
+
const x = dragX.value;
|
|
58
|
+
const y = dragY.value;
|
|
59
|
+
const vx = e.velocityX;
|
|
60
|
+
const vy = e.velocityY;
|
|
61
|
+
reactNativeReanimated.runOnJS(onDragEnd)({ x, y, velocity: { x: vx, y: vy } });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
return pan;
|
|
65
|
+
}, [
|
|
66
|
+
lockX,
|
|
67
|
+
lockY,
|
|
68
|
+
left,
|
|
69
|
+
right,
|
|
70
|
+
top,
|
|
71
|
+
bottom,
|
|
72
|
+
elasticCoef,
|
|
73
|
+
onDragStart,
|
|
74
|
+
onDragEnd,
|
|
75
|
+
dragX,
|
|
76
|
+
dragY,
|
|
77
|
+
startX,
|
|
78
|
+
startY,
|
|
79
|
+
isDragging
|
|
80
|
+
]);
|
|
81
|
+
const animatedStyle = reactNativeReanimated.useAnimatedStyle(() => ({
|
|
82
|
+
transform: [{ translateX: dragX.value }, { translateY: dragY.value }]
|
|
83
|
+
}));
|
|
84
|
+
return { gesture, animatedStyle, dragX, dragY, isDragging };
|
|
85
|
+
}
|
|
86
|
+
function applyBounds(value, min, max, elastic) {
|
|
87
|
+
"worklet";
|
|
88
|
+
if (min !== void 0 && value < min) {
|
|
89
|
+
return elastic > 0 ? min + (value - min) * elastic : min;
|
|
90
|
+
}
|
|
91
|
+
if (max !== void 0 && value > max) {
|
|
92
|
+
return elastic > 0 ? max + (value - max) * elastic : max;
|
|
93
|
+
}
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
var DEFAULT_DIRECTIONS = ["left", "right", "up", "down"];
|
|
97
|
+
function useSwipe(options = {}) {
|
|
98
|
+
const {
|
|
99
|
+
directions = DEFAULT_DIRECTIONS,
|
|
100
|
+
distanceThreshold = 80,
|
|
101
|
+
velocityThreshold = 800,
|
|
102
|
+
onSwipe
|
|
103
|
+
} = options;
|
|
104
|
+
const swipeX = reactNativeReanimated.useSharedValue(0);
|
|
105
|
+
const swipeY = reactNativeReanimated.useSharedValue(0);
|
|
106
|
+
const isActive = reactNativeReanimated.useSharedValue(false);
|
|
107
|
+
const allowLeft = directions.includes("left");
|
|
108
|
+
const allowRight = directions.includes("right");
|
|
109
|
+
const allowUp = directions.includes("up");
|
|
110
|
+
const allowDown = directions.includes("down");
|
|
111
|
+
const gesture = react.useMemo(() => {
|
|
112
|
+
const pan = reactNativeGestureHandler.Gesture.Pan().onStart(() => {
|
|
113
|
+
"worklet";
|
|
114
|
+
isActive.value = true;
|
|
115
|
+
}).onUpdate((e) => {
|
|
116
|
+
"worklet";
|
|
117
|
+
swipeX.value = e.translationX;
|
|
118
|
+
swipeY.value = e.translationY;
|
|
119
|
+
}).onEnd((e) => {
|
|
120
|
+
"worklet";
|
|
121
|
+
isActive.value = false;
|
|
122
|
+
const direction = pickDirection(
|
|
123
|
+
e.translationX,
|
|
124
|
+
e.translationY,
|
|
125
|
+
e.velocityX,
|
|
126
|
+
e.velocityY,
|
|
127
|
+
distanceThreshold,
|
|
128
|
+
velocityThreshold,
|
|
129
|
+
allowLeft,
|
|
130
|
+
allowRight,
|
|
131
|
+
allowUp,
|
|
132
|
+
allowDown
|
|
133
|
+
);
|
|
134
|
+
if (direction !== null && onSwipe) {
|
|
135
|
+
const isHoriz = direction === "left" || direction === "right";
|
|
136
|
+
const distance = isHoriz ? Math.abs(e.translationX) : Math.abs(e.translationY);
|
|
137
|
+
const velocity = isHoriz ? Math.abs(e.velocityX) : Math.abs(e.velocityY);
|
|
138
|
+
reactNativeReanimated.runOnJS(onSwipe)(direction, { distance, velocity });
|
|
139
|
+
}
|
|
140
|
+
swipeX.value = reactNativeReanimated.withSpring(0);
|
|
141
|
+
swipeY.value = reactNativeReanimated.withSpring(0);
|
|
142
|
+
}).onFinalize(() => {
|
|
143
|
+
"worklet";
|
|
144
|
+
isActive.value = false;
|
|
145
|
+
});
|
|
146
|
+
return pan;
|
|
147
|
+
}, [
|
|
148
|
+
distanceThreshold,
|
|
149
|
+
velocityThreshold,
|
|
150
|
+
allowLeft,
|
|
151
|
+
allowRight,
|
|
152
|
+
allowUp,
|
|
153
|
+
allowDown,
|
|
154
|
+
onSwipe,
|
|
155
|
+
swipeX,
|
|
156
|
+
swipeY,
|
|
157
|
+
isActive
|
|
158
|
+
]);
|
|
159
|
+
const animatedStyle = reactNativeReanimated.useAnimatedStyle(() => ({
|
|
160
|
+
transform: [{ translateX: swipeX.value }, { translateY: swipeY.value }]
|
|
161
|
+
}));
|
|
162
|
+
return { gesture, animatedStyle, swipeX, swipeY, isActive };
|
|
163
|
+
}
|
|
164
|
+
function pickDirection(tx, ty, vx, vy, distanceThreshold, velocityThreshold, allowLeft, allowRight, allowUp, allowDown) {
|
|
165
|
+
"worklet";
|
|
166
|
+
const absX = Math.abs(tx);
|
|
167
|
+
const absY = Math.abs(ty);
|
|
168
|
+
if (absX >= absY) {
|
|
169
|
+
const meets2 = absX >= distanceThreshold || Math.abs(vx) >= velocityThreshold;
|
|
170
|
+
if (!meets2) return null;
|
|
171
|
+
if (tx < 0 && allowLeft) return "left";
|
|
172
|
+
if (tx > 0 && allowRight) return "right";
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
const meets = absY >= distanceThreshold || Math.abs(vy) >= velocityThreshold;
|
|
176
|
+
if (!meets) return null;
|
|
177
|
+
if (ty < 0 && allowUp) return "up";
|
|
178
|
+
if (ty > 0 && allowDown) return "down";
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
function usePan(options = {}) {
|
|
182
|
+
const { constraints, deceleration, disableMomentum = false } = options;
|
|
183
|
+
const panX = reactNativeReanimated.useSharedValue(0);
|
|
184
|
+
const panY = reactNativeReanimated.useSharedValue(0);
|
|
185
|
+
const startX = reactNativeReanimated.useSharedValue(0);
|
|
186
|
+
const startY = reactNativeReanimated.useSharedValue(0);
|
|
187
|
+
const isPanning = reactNativeReanimated.useSharedValue(false);
|
|
188
|
+
const left = constraints?.left;
|
|
189
|
+
const right = constraints?.right;
|
|
190
|
+
const top = constraints?.top;
|
|
191
|
+
const bottom = constraints?.bottom;
|
|
192
|
+
const decel = deceleration;
|
|
193
|
+
const gesture = react.useMemo(() => {
|
|
194
|
+
const pan = reactNativeGestureHandler.Gesture.Pan().onStart(() => {
|
|
195
|
+
"worklet";
|
|
196
|
+
startX.value = panX.value;
|
|
197
|
+
startY.value = panY.value;
|
|
198
|
+
isPanning.value = true;
|
|
199
|
+
}).onUpdate((e) => {
|
|
200
|
+
"worklet";
|
|
201
|
+
panX.value = clamp(startX.value + e.translationX, left, right);
|
|
202
|
+
panY.value = clamp(startY.value + e.translationY, top, bottom);
|
|
203
|
+
}).onEnd((e) => {
|
|
204
|
+
"worklet";
|
|
205
|
+
isPanning.value = false;
|
|
206
|
+
if (disableMomentum) return;
|
|
207
|
+
const clampX = boundsTuple(left, right);
|
|
208
|
+
const clampY = boundsTuple(top, bottom);
|
|
209
|
+
panX.value = reactNativeReanimated.withDecay(decayConfig(e.velocityX, decel, clampX));
|
|
210
|
+
panY.value = reactNativeReanimated.withDecay(decayConfig(e.velocityY, decel, clampY));
|
|
211
|
+
}).onFinalize(() => {
|
|
212
|
+
"worklet";
|
|
213
|
+
isPanning.value = false;
|
|
214
|
+
});
|
|
215
|
+
return pan;
|
|
216
|
+
}, [
|
|
217
|
+
left,
|
|
218
|
+
right,
|
|
219
|
+
top,
|
|
220
|
+
bottom,
|
|
221
|
+
decel,
|
|
222
|
+
disableMomentum,
|
|
223
|
+
panX,
|
|
224
|
+
panY,
|
|
225
|
+
startX,
|
|
226
|
+
startY,
|
|
227
|
+
isPanning
|
|
228
|
+
]);
|
|
229
|
+
const animatedStyle = reactNativeReanimated.useAnimatedStyle(() => ({
|
|
230
|
+
transform: [{ translateX: panX.value }, { translateY: panY.value }]
|
|
231
|
+
}));
|
|
232
|
+
return { gesture, animatedStyle, panX, panY, isPanning };
|
|
233
|
+
}
|
|
234
|
+
function clamp(value, min, max) {
|
|
235
|
+
"worklet";
|
|
236
|
+
if (min !== void 0 && value < min) return min;
|
|
237
|
+
if (max !== void 0 && value > max) return max;
|
|
238
|
+
return value;
|
|
239
|
+
}
|
|
240
|
+
function boundsTuple(min, max) {
|
|
241
|
+
"worklet";
|
|
242
|
+
if (min === void 0 && max === void 0) return void 0;
|
|
243
|
+
return [min ?? Number.NEGATIVE_INFINITY, max ?? Number.POSITIVE_INFINITY];
|
|
244
|
+
}
|
|
245
|
+
function decayConfig(velocity, deceleration, clamp2) {
|
|
246
|
+
"worklet";
|
|
247
|
+
const cfg = {
|
|
248
|
+
velocity
|
|
249
|
+
};
|
|
250
|
+
if (deceleration !== void 0) cfg.deceleration = deceleration;
|
|
251
|
+
if (clamp2 !== void 0) cfg.clamp = clamp2;
|
|
252
|
+
return cfg;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
exports.useDrag = useDrag;
|
|
256
|
+
exports.usePan = usePan;
|
|
257
|
+
exports.useSwipe = useSwipe;
|
|
258
|
+
//# sourceMappingURL=index.js.map
|
|
259
|
+
//# sourceMappingURL=index.js.map
|