@onlynative/inertia 0.0.1-alpha.7 → 0.0.1-alpha.9
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/README.md +1 -1
- package/dist/gestureLayer/index.d.mts +119 -0
- package/dist/gestureLayer/index.d.ts +119 -0
- package/dist/gestureLayer/index.js +346 -0
- package/dist/gestureLayer/index.js.map +1 -0
- package/dist/gestureLayer/index.mjs +344 -0
- package/dist/gestureLayer/index.mjs.map +1 -0
- package/dist/index.d.mts +114 -74
- package/dist/index.d.ts +114 -74
- package/dist/index.js +388 -1542
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +388 -1545
- package/dist/index.mjs.map +1 -1
- package/dist/motion/Image.d.mts +1 -1
- package/dist/motion/Image.d.ts +1 -1
- package/dist/motion/Image.js +244 -1462
- package/dist/motion/Image.js.map +1 -1
- package/dist/motion/Image.mjs +247 -1465
- package/dist/motion/Image.mjs.map +1 -1
- package/dist/motion/Pressable.d.mts +1 -1
- package/dist/motion/Pressable.d.ts +1 -1
- package/dist/motion/Pressable.js +244 -1462
- package/dist/motion/Pressable.js.map +1 -1
- package/dist/motion/Pressable.mjs +247 -1465
- package/dist/motion/Pressable.mjs.map +1 -1
- package/dist/motion/ScrollView.d.mts +1 -1
- package/dist/motion/ScrollView.d.ts +1 -1
- package/dist/motion/ScrollView.js +244 -1462
- package/dist/motion/ScrollView.js.map +1 -1
- package/dist/motion/ScrollView.mjs +247 -1465
- package/dist/motion/ScrollView.mjs.map +1 -1
- package/dist/motion/Text.d.mts +1 -1
- package/dist/motion/Text.d.ts +1 -1
- package/dist/motion/Text.js +244 -1462
- package/dist/motion/Text.js.map +1 -1
- package/dist/motion/Text.mjs +247 -1465
- package/dist/motion/Text.mjs.map +1 -1
- package/dist/motion/View.d.mts +1 -1
- package/dist/motion/View.d.ts +1 -1
- package/dist/motion/View.js +244 -1462
- package/dist/motion/View.js.map +1 -1
- package/dist/motion/View.mjs +247 -1465
- package/dist/motion/View.mjs.map +1 -1
- package/dist/touch/index.d.mts +146 -0
- package/dist/touch/index.d.ts +146 -0
- package/dist/touch/index.js +166 -0
- package/dist/touch/index.js.map +1 -0
- package/dist/touch/index.mjs +164 -0
- package/dist/touch/index.mjs.map +1 -0
- package/dist/{types-NmNeJjo1.d.mts → types-cU43dEmH.d.mts} +64 -17
- package/dist/{types-NmNeJjo1.d.ts → types-cU43dEmH.d.ts} +64 -17
- package/dist/useGesture-B7A_1DVg.d.ts +84 -0
- package/dist/useGesture-cimMrzC1.d.mts +84 -0
- package/jest-setup.js +4 -0
- package/llms.txt +12 -3
- package/package.json +22 -2
- package/src/__type-tests__/variants.test-d.tsx +67 -0
- package/src/gestureLayer/index.ts +21 -0
- package/src/gestureLayer/useGestureLayer.ts +285 -0
- package/src/index.ts +7 -0
- package/src/layout/index.ts +15 -0
- package/src/layout/sharedRegistry.ts +111 -0
- package/src/layout/useSharedLayout.ts +289 -0
- package/src/motion/createMotionComponent.tsx +123 -37
- package/src/motion/installCheck.ts +7 -11
- package/src/touch/index.ts +18 -0
- package/src/touch/useTouchDrag.ts +289 -0
- package/src/types.ts +79 -20
- package/src/values/index.ts +11 -0
- package/src/values/useBooleanSpring.ts +33 -0
- package/src/values/useColorTransition.ts +72 -0
- package/src/values/useShadow.ts +116 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { PanResponderInstance } from 'react-native';
|
|
2
|
+
import { useAnimatedStyle, SharedValue } from 'react-native-reanimated';
|
|
3
|
+
import { T as TransitionConfig } from '../types-cU43dEmH.mjs';
|
|
4
|
+
import 'react';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Same drag-result shape as `useDrag` from `@onlynative/inertia-gestures`,
|
|
8
|
+
* minus the `gesture` field (PanResponder spreads handlers, no
|
|
9
|
+
* `<GestureDetector>` wrapper). The shared values + animatedStyle are
|
|
10
|
+
* interchangeable across both hooks; consumers can swap implementations
|
|
11
|
+
* without touching their `useAnimatedStyle` consumers.
|
|
12
|
+
*/
|
|
13
|
+
interface UseTouchDragResult {
|
|
14
|
+
/** Spread onto a `View` / `Pressable` to install the pan responder. */
|
|
15
|
+
panHandlers: PanResponderInstance['panHandlers'];
|
|
16
|
+
/** Stable animated `transform` style. */
|
|
17
|
+
animatedStyle: ReturnType<typeof useAnimatedStyle>;
|
|
18
|
+
/** Live x translation, persistent across gestures. */
|
|
19
|
+
dragX: SharedValue<number>;
|
|
20
|
+
/** Live y translation, persistent across gestures. */
|
|
21
|
+
dragY: SharedValue<number>;
|
|
22
|
+
/** True while the gesture is active. */
|
|
23
|
+
isDragging: SharedValue<boolean>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Release transition shape for PanResponder's JS-thread `onRelease`. Mirrors
|
|
27
|
+
* the gesture-handler adapter's `ReleaseTransition` but with `to` typed as
|
|
28
|
+
* required for spring/timing/no-animation (decay omits it).
|
|
29
|
+
*/
|
|
30
|
+
type TouchReleaseTransition = (TransitionConfig & {
|
|
31
|
+
type: 'spring';
|
|
32
|
+
to: number;
|
|
33
|
+
}) | (TransitionConfig & {
|
|
34
|
+
type: 'timing';
|
|
35
|
+
to: number;
|
|
36
|
+
}) | (TransitionConfig & {
|
|
37
|
+
type: 'decay';
|
|
38
|
+
}) | (TransitionConfig & {
|
|
39
|
+
type: 'no-animation';
|
|
40
|
+
to: number;
|
|
41
|
+
});
|
|
42
|
+
interface TouchReleaseInfo {
|
|
43
|
+
x: number;
|
|
44
|
+
y: number;
|
|
45
|
+
velocity: {
|
|
46
|
+
x: number;
|
|
47
|
+
y: number;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
interface TouchReleaseResult {
|
|
51
|
+
x?: TouchReleaseTransition;
|
|
52
|
+
y?: TouchReleaseTransition;
|
|
53
|
+
}
|
|
54
|
+
interface UseTouchDragOptions {
|
|
55
|
+
/**
|
|
56
|
+
* Restrict the drag to one axis. Defaults to `'both'`. When `'x'` is set
|
|
57
|
+
* the y-axis shared value never updates (and vice versa); velocity is
|
|
58
|
+
* still reported on both for `onDragEnd`.
|
|
59
|
+
*/
|
|
60
|
+
axis?: 'x' | 'y' | 'both';
|
|
61
|
+
/**
|
|
62
|
+
* Travel bounds (px from resting). Each side is independently optional.
|
|
63
|
+
* Out-of-bounds values clamp to the limit unless `elastic > 0`.
|
|
64
|
+
*/
|
|
65
|
+
constraints?: {
|
|
66
|
+
left?: number;
|
|
67
|
+
right?: number;
|
|
68
|
+
top?: number;
|
|
69
|
+
bottom?: number;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Rubber-band coefficient applied to overshoot past `constraints`. `0`
|
|
73
|
+
* (default) hard-clamps; `0.2`-`0.4` is a typical Framer-Motion feel.
|
|
74
|
+
*/
|
|
75
|
+
elastic?: number;
|
|
76
|
+
/**
|
|
77
|
+
* Fires when the user starts dragging. JS thread.
|
|
78
|
+
*/
|
|
79
|
+
onDragStart?: () => void;
|
|
80
|
+
/**
|
|
81
|
+
* Fires when the user releases or the gesture terminates. JS thread.
|
|
82
|
+
*
|
|
83
|
+
* Velocity is in px/sec to match the `@onlynative/inertia-gestures` API
|
|
84
|
+
* (PanResponder's native `vx` / `vy` are px/ms; the hook normalizes).
|
|
85
|
+
*/
|
|
86
|
+
onDragEnd?: (info: TouchReleaseInfo) => void;
|
|
87
|
+
/**
|
|
88
|
+
* Optional release-animation callback. Return per-axis release transitions
|
|
89
|
+
* to animate the SVs to a settled position via Inertia's transition
|
|
90
|
+
* resolver — spring snap-to-tick, decay with bounds, timing settle.
|
|
91
|
+
*
|
|
92
|
+
* Unlike the gesture-handler version, this callback runs on the **JS
|
|
93
|
+
* thread** (PanResponder is JS-only). The returned transitions still drive
|
|
94
|
+
* UI-thread animations via Reanimated — only the decision logic is JS-side.
|
|
95
|
+
*/
|
|
96
|
+
onRelease?: (info: TouchReleaseInfo) => TouchReleaseResult | void;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* PanResponder-backed drag hook. Pointer-equivalent of `useDrag` from
|
|
100
|
+
* `@onlynative/inertia-gestures`, with two differences:
|
|
101
|
+
*
|
|
102
|
+
* 1. No `react-native-gesture-handler` peer dep required — PanResponder is
|
|
103
|
+
* built into React Native, so this lives in core.
|
|
104
|
+
* 2. Returns `panHandlers` to spread on a `View` / `Pressable` instead of
|
|
105
|
+
* a `gesture` to plug into `<GestureDetector>`.
|
|
106
|
+
*
|
|
107
|
+
* Use this when:
|
|
108
|
+
* - You need keyboard a11y alongside drag (a slider with arrow-key step,
|
|
109
|
+
* a scrollbar with `PageUp` / `PageDown`). PanResponder composes
|
|
110
|
+
* cleanly with `onKeyDown`; gesture-handler doesn't surface keyboard.
|
|
111
|
+
* - You don't want to take `react-native-gesture-handler` as a dependency
|
|
112
|
+
* (smaller bundle, simpler install).
|
|
113
|
+
*
|
|
114
|
+
* Skip this when:
|
|
115
|
+
* - You're already using `react-native-gesture-handler` elsewhere (use
|
|
116
|
+
* `useDrag` from `@onlynative/inertia-gestures` for consistency and
|
|
117
|
+
* better worklet-thread fidelity on release velocity).
|
|
118
|
+
* - You need momentum semantics like the gesture-handler `usePan` —
|
|
119
|
+
* PanResponder's release velocity is JS-thread and slightly less precise.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```tsx
|
|
123
|
+
* import { useTouchDrag } from '@onlynative/inertia/touch'
|
|
124
|
+
*
|
|
125
|
+
* function Slider({ ticks }: { ticks: number[] }) {
|
|
126
|
+
* const drag = useTouchDrag({
|
|
127
|
+
* axis: 'x',
|
|
128
|
+
* constraints: { left: 0, right: 280 },
|
|
129
|
+
* onRelease: (e) => {
|
|
130
|
+
* const snap = nearestTick(e.x, ticks)
|
|
131
|
+
* return { x: { type: 'spring', to: snap, velocity: e.velocity.x } }
|
|
132
|
+
* },
|
|
133
|
+
* })
|
|
134
|
+
*
|
|
135
|
+
* return (
|
|
136
|
+
* <Motion.View
|
|
137
|
+
* style={[styles.thumb, drag.animatedStyle]}
|
|
138
|
+
* {...drag.panHandlers}
|
|
139
|
+
* />
|
|
140
|
+
* )
|
|
141
|
+
* }
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
declare function useTouchDrag(options?: UseTouchDragOptions): UseTouchDragResult;
|
|
145
|
+
|
|
146
|
+
export { type TouchReleaseInfo, type TouchReleaseResult, type TouchReleaseTransition, type UseTouchDragOptions, type UseTouchDragResult, useTouchDrag };
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { PanResponderInstance } from 'react-native';
|
|
2
|
+
import { useAnimatedStyle, SharedValue } from 'react-native-reanimated';
|
|
3
|
+
import { T as TransitionConfig } from '../types-cU43dEmH.js';
|
|
4
|
+
import 'react';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Same drag-result shape as `useDrag` from `@onlynative/inertia-gestures`,
|
|
8
|
+
* minus the `gesture` field (PanResponder spreads handlers, no
|
|
9
|
+
* `<GestureDetector>` wrapper). The shared values + animatedStyle are
|
|
10
|
+
* interchangeable across both hooks; consumers can swap implementations
|
|
11
|
+
* without touching their `useAnimatedStyle` consumers.
|
|
12
|
+
*/
|
|
13
|
+
interface UseTouchDragResult {
|
|
14
|
+
/** Spread onto a `View` / `Pressable` to install the pan responder. */
|
|
15
|
+
panHandlers: PanResponderInstance['panHandlers'];
|
|
16
|
+
/** Stable animated `transform` style. */
|
|
17
|
+
animatedStyle: ReturnType<typeof useAnimatedStyle>;
|
|
18
|
+
/** Live x translation, persistent across gestures. */
|
|
19
|
+
dragX: SharedValue<number>;
|
|
20
|
+
/** Live y translation, persistent across gestures. */
|
|
21
|
+
dragY: SharedValue<number>;
|
|
22
|
+
/** True while the gesture is active. */
|
|
23
|
+
isDragging: SharedValue<boolean>;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Release transition shape for PanResponder's JS-thread `onRelease`. Mirrors
|
|
27
|
+
* the gesture-handler adapter's `ReleaseTransition` but with `to` typed as
|
|
28
|
+
* required for spring/timing/no-animation (decay omits it).
|
|
29
|
+
*/
|
|
30
|
+
type TouchReleaseTransition = (TransitionConfig & {
|
|
31
|
+
type: 'spring';
|
|
32
|
+
to: number;
|
|
33
|
+
}) | (TransitionConfig & {
|
|
34
|
+
type: 'timing';
|
|
35
|
+
to: number;
|
|
36
|
+
}) | (TransitionConfig & {
|
|
37
|
+
type: 'decay';
|
|
38
|
+
}) | (TransitionConfig & {
|
|
39
|
+
type: 'no-animation';
|
|
40
|
+
to: number;
|
|
41
|
+
});
|
|
42
|
+
interface TouchReleaseInfo {
|
|
43
|
+
x: number;
|
|
44
|
+
y: number;
|
|
45
|
+
velocity: {
|
|
46
|
+
x: number;
|
|
47
|
+
y: number;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
interface TouchReleaseResult {
|
|
51
|
+
x?: TouchReleaseTransition;
|
|
52
|
+
y?: TouchReleaseTransition;
|
|
53
|
+
}
|
|
54
|
+
interface UseTouchDragOptions {
|
|
55
|
+
/**
|
|
56
|
+
* Restrict the drag to one axis. Defaults to `'both'`. When `'x'` is set
|
|
57
|
+
* the y-axis shared value never updates (and vice versa); velocity is
|
|
58
|
+
* still reported on both for `onDragEnd`.
|
|
59
|
+
*/
|
|
60
|
+
axis?: 'x' | 'y' | 'both';
|
|
61
|
+
/**
|
|
62
|
+
* Travel bounds (px from resting). Each side is independently optional.
|
|
63
|
+
* Out-of-bounds values clamp to the limit unless `elastic > 0`.
|
|
64
|
+
*/
|
|
65
|
+
constraints?: {
|
|
66
|
+
left?: number;
|
|
67
|
+
right?: number;
|
|
68
|
+
top?: number;
|
|
69
|
+
bottom?: number;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Rubber-band coefficient applied to overshoot past `constraints`. `0`
|
|
73
|
+
* (default) hard-clamps; `0.2`-`0.4` is a typical Framer-Motion feel.
|
|
74
|
+
*/
|
|
75
|
+
elastic?: number;
|
|
76
|
+
/**
|
|
77
|
+
* Fires when the user starts dragging. JS thread.
|
|
78
|
+
*/
|
|
79
|
+
onDragStart?: () => void;
|
|
80
|
+
/**
|
|
81
|
+
* Fires when the user releases or the gesture terminates. JS thread.
|
|
82
|
+
*
|
|
83
|
+
* Velocity is in px/sec to match the `@onlynative/inertia-gestures` API
|
|
84
|
+
* (PanResponder's native `vx` / `vy` are px/ms; the hook normalizes).
|
|
85
|
+
*/
|
|
86
|
+
onDragEnd?: (info: TouchReleaseInfo) => void;
|
|
87
|
+
/**
|
|
88
|
+
* Optional release-animation callback. Return per-axis release transitions
|
|
89
|
+
* to animate the SVs to a settled position via Inertia's transition
|
|
90
|
+
* resolver — spring snap-to-tick, decay with bounds, timing settle.
|
|
91
|
+
*
|
|
92
|
+
* Unlike the gesture-handler version, this callback runs on the **JS
|
|
93
|
+
* thread** (PanResponder is JS-only). The returned transitions still drive
|
|
94
|
+
* UI-thread animations via Reanimated — only the decision logic is JS-side.
|
|
95
|
+
*/
|
|
96
|
+
onRelease?: (info: TouchReleaseInfo) => TouchReleaseResult | void;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* PanResponder-backed drag hook. Pointer-equivalent of `useDrag` from
|
|
100
|
+
* `@onlynative/inertia-gestures`, with two differences:
|
|
101
|
+
*
|
|
102
|
+
* 1. No `react-native-gesture-handler` peer dep required — PanResponder is
|
|
103
|
+
* built into React Native, so this lives in core.
|
|
104
|
+
* 2. Returns `panHandlers` to spread on a `View` / `Pressable` instead of
|
|
105
|
+
* a `gesture` to plug into `<GestureDetector>`.
|
|
106
|
+
*
|
|
107
|
+
* Use this when:
|
|
108
|
+
* - You need keyboard a11y alongside drag (a slider with arrow-key step,
|
|
109
|
+
* a scrollbar with `PageUp` / `PageDown`). PanResponder composes
|
|
110
|
+
* cleanly with `onKeyDown`; gesture-handler doesn't surface keyboard.
|
|
111
|
+
* - You don't want to take `react-native-gesture-handler` as a dependency
|
|
112
|
+
* (smaller bundle, simpler install).
|
|
113
|
+
*
|
|
114
|
+
* Skip this when:
|
|
115
|
+
* - You're already using `react-native-gesture-handler` elsewhere (use
|
|
116
|
+
* `useDrag` from `@onlynative/inertia-gestures` for consistency and
|
|
117
|
+
* better worklet-thread fidelity on release velocity).
|
|
118
|
+
* - You need momentum semantics like the gesture-handler `usePan` —
|
|
119
|
+
* PanResponder's release velocity is JS-thread and slightly less precise.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```tsx
|
|
123
|
+
* import { useTouchDrag } from '@onlynative/inertia/touch'
|
|
124
|
+
*
|
|
125
|
+
* function Slider({ ticks }: { ticks: number[] }) {
|
|
126
|
+
* const drag = useTouchDrag({
|
|
127
|
+
* axis: 'x',
|
|
128
|
+
* constraints: { left: 0, right: 280 },
|
|
129
|
+
* onRelease: (e) => {
|
|
130
|
+
* const snap = nearestTick(e.x, ticks)
|
|
131
|
+
* return { x: { type: 'spring', to: snap, velocity: e.velocity.x } }
|
|
132
|
+
* },
|
|
133
|
+
* })
|
|
134
|
+
*
|
|
135
|
+
* return (
|
|
136
|
+
* <Motion.View
|
|
137
|
+
* style={[styles.thumb, drag.animatedStyle]}
|
|
138
|
+
* {...drag.panHandlers}
|
|
139
|
+
* />
|
|
140
|
+
* )
|
|
141
|
+
* }
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
declare function useTouchDrag(options?: UseTouchDragOptions): UseTouchDragResult;
|
|
145
|
+
|
|
146
|
+
export { type TouchReleaseInfo, type TouchReleaseResult, type TouchReleaseTransition, type UseTouchDragOptions, type UseTouchDragResult, useTouchDrag };
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var reactNative = require('react-native');
|
|
5
|
+
var reactNativeReanimated = require('react-native-reanimated');
|
|
6
|
+
|
|
7
|
+
// src/touch/useTouchDrag.ts
|
|
8
|
+
|
|
9
|
+
// src/transitions/spring.ts
|
|
10
|
+
var DEFAULT_SPRING = {
|
|
11
|
+
tension: 170,
|
|
12
|
+
friction: 26,
|
|
13
|
+
mass: 1
|
|
14
|
+
};
|
|
15
|
+
function springToReanimated(t) {
|
|
16
|
+
"worklet";
|
|
17
|
+
return {
|
|
18
|
+
stiffness: t.tension ?? DEFAULT_SPRING.tension,
|
|
19
|
+
damping: t.friction ?? DEFAULT_SPRING.friction,
|
|
20
|
+
mass: t.mass ?? DEFAULT_SPRING.mass,
|
|
21
|
+
velocity: t.velocity,
|
|
22
|
+
restSpeedThreshold: t.restSpeedThreshold,
|
|
23
|
+
restDisplacementThreshold: t.restDisplacementThreshold
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
var DEFAULT_TIMING_DURATION = 250;
|
|
27
|
+
function buildReleaseAnimation(transition, toValue) {
|
|
28
|
+
"worklet";
|
|
29
|
+
if (transition.type === "no-animation") return toValue;
|
|
30
|
+
if (transition.type === "decay") {
|
|
31
|
+
const cfg = { velocity: transition.velocity ?? 0 };
|
|
32
|
+
if (transition.deceleration !== void 0) {
|
|
33
|
+
cfg.deceleration = transition.deceleration;
|
|
34
|
+
}
|
|
35
|
+
if (transition.clamp !== void 0) cfg.clamp = transition.clamp;
|
|
36
|
+
return reactNativeReanimated.withDecay(cfg);
|
|
37
|
+
}
|
|
38
|
+
if (transition.type === "timing") {
|
|
39
|
+
const e = transition.easing;
|
|
40
|
+
const easingFn = e && typeof e === "object" && "factory" in e ? e.factory() : e ?? reactNativeReanimated.Easing.inOut(reactNativeReanimated.Easing.ease);
|
|
41
|
+
return reactNativeReanimated.withTiming(toValue, {
|
|
42
|
+
duration: transition.duration ?? DEFAULT_TIMING_DURATION,
|
|
43
|
+
easing: easingFn
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return reactNativeReanimated.withSpring(toValue, springToReanimated(transition));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/touch/useTouchDrag.ts
|
|
50
|
+
function useTouchDrag(options = {}) {
|
|
51
|
+
const { axis = "both", constraints, elastic = 0 } = options;
|
|
52
|
+
const dragX = reactNativeReanimated.useSharedValue(0);
|
|
53
|
+
const dragY = reactNativeReanimated.useSharedValue(0);
|
|
54
|
+
const startX = reactNativeReanimated.useSharedValue(0);
|
|
55
|
+
const startY = reactNativeReanimated.useSharedValue(0);
|
|
56
|
+
const isDragging = reactNativeReanimated.useSharedValue(false);
|
|
57
|
+
const lockX = axis !== "y";
|
|
58
|
+
const lockY = axis !== "x";
|
|
59
|
+
const left = constraints?.left;
|
|
60
|
+
const right = constraints?.right;
|
|
61
|
+
const top = constraints?.top;
|
|
62
|
+
const bottom = constraints?.bottom;
|
|
63
|
+
const elasticCoef = elastic;
|
|
64
|
+
const { onDragStart, onDragEnd, onRelease } = options;
|
|
65
|
+
const responder = react.useMemo(
|
|
66
|
+
() => buildResponder(),
|
|
67
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
68
|
+
[
|
|
69
|
+
lockX,
|
|
70
|
+
lockY,
|
|
71
|
+
left,
|
|
72
|
+
right,
|
|
73
|
+
top,
|
|
74
|
+
bottom,
|
|
75
|
+
elasticCoef,
|
|
76
|
+
onDragStart,
|
|
77
|
+
onDragEnd,
|
|
78
|
+
onRelease
|
|
79
|
+
]
|
|
80
|
+
);
|
|
81
|
+
function buildResponder() {
|
|
82
|
+
const handleEnd = (g) => {
|
|
83
|
+
isDragging.value = false;
|
|
84
|
+
const x = dragX.value;
|
|
85
|
+
const y = dragY.value;
|
|
86
|
+
const vx = g.vx * 1e3;
|
|
87
|
+
const vy = g.vy * 1e3;
|
|
88
|
+
if (onRelease) {
|
|
89
|
+
const result = onRelease({ x, y, velocity: { x: vx, y: vy } });
|
|
90
|
+
if (result) {
|
|
91
|
+
if (result.x && lockX) {
|
|
92
|
+
const toX = "to" in result.x ? result.x.to : x;
|
|
93
|
+
dragX.value = buildReleaseAnimation(
|
|
94
|
+
result.x,
|
|
95
|
+
toX
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
if (result.y && lockY) {
|
|
99
|
+
const toY = "to" in result.y ? result.y.to : y;
|
|
100
|
+
dragY.value = buildReleaseAnimation(
|
|
101
|
+
result.y,
|
|
102
|
+
toY
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (onDragEnd) onDragEnd({ x, y, velocity: { x: vx, y: vy } });
|
|
108
|
+
};
|
|
109
|
+
return reactNative.PanResponder.create({
|
|
110
|
+
// Always claim the start so taps that turn into drags don't slip
|
|
111
|
+
// through to a parent ScrollView. Consumers can compose their own
|
|
112
|
+
// capture predicates by wrapping the returned `panHandlers`.
|
|
113
|
+
onStartShouldSetPanResponder: () => true,
|
|
114
|
+
onMoveShouldSetPanResponder: () => true,
|
|
115
|
+
onPanResponderGrant: () => {
|
|
116
|
+
startX.value = dragX.value;
|
|
117
|
+
startY.value = dragY.value;
|
|
118
|
+
isDragging.value = true;
|
|
119
|
+
if (onDragStart) onDragStart();
|
|
120
|
+
},
|
|
121
|
+
onPanResponderMove: (_e, g) => {
|
|
122
|
+
if (lockX) {
|
|
123
|
+
dragX.value = applyBounds(
|
|
124
|
+
startX.value + g.dx,
|
|
125
|
+
left,
|
|
126
|
+
right,
|
|
127
|
+
elasticCoef
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
if (lockY) {
|
|
131
|
+
dragY.value = applyBounds(
|
|
132
|
+
startY.value + g.dy,
|
|
133
|
+
top,
|
|
134
|
+
bottom,
|
|
135
|
+
elasticCoef
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
onPanResponderRelease: (_e, g) => handleEnd(g),
|
|
140
|
+
onPanResponderTerminate: (_e, g) => handleEnd(g)
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
const animatedStyle = reactNativeReanimated.useAnimatedStyle(() => ({
|
|
144
|
+
transform: [{ translateX: dragX.value }, { translateY: dragY.value }]
|
|
145
|
+
}));
|
|
146
|
+
return {
|
|
147
|
+
panHandlers: responder.panHandlers,
|
|
148
|
+
animatedStyle,
|
|
149
|
+
dragX,
|
|
150
|
+
dragY,
|
|
151
|
+
isDragging
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function applyBounds(value, min, max, elastic) {
|
|
155
|
+
if (min !== void 0 && value < min) {
|
|
156
|
+
return elastic > 0 ? min + (value - min) * elastic : min;
|
|
157
|
+
}
|
|
158
|
+
if (max !== void 0 && value > max) {
|
|
159
|
+
return elastic > 0 ? max + (value - max) * elastic : max;
|
|
160
|
+
}
|
|
161
|
+
return value;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
exports.useTouchDrag = useTouchDrag;
|
|
165
|
+
//# sourceMappingURL=index.js.map
|
|
166
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/transitions/spring.ts","../../src/transitions/runtime.ts","../../src/touch/useTouchDrag.ts"],"names":["withDecay","Easing","withTiming","withSpring","useSharedValue","useMemo","PanResponder","useAnimatedStyle"],"mappings":"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","file":"index.js","sourcesContent":["import { type SpringTransition } from '../types'\n\n/**\n * Default spring physics, expressed in react-spring vocabulary.\n *\n * `tension: 170` / `friction: 26` / `mass: 1` was picked over Reanimated's\n * raw `stiffness: 100` / `damping: 10` default because the raw default\n * overshoots noticeably for the small (~100px) translates that dominate\n * UI work — buttons, sheets, popovers. These numbers settle in ~350ms with\n * a single, almost-imperceptible overshoot, which matches the perceptual\n * target the rest of the library is tuned against.\n */\nexport const DEFAULT_SPRING: Required<\n Pick<SpringTransition, 'tension' | 'friction' | 'mass'>\n> = {\n tension: 170,\n friction: 26,\n mass: 1,\n}\n\n/**\n * Convert public react-spring vocabulary (`tension` / `friction` / `mass`)\n * to Reanimated's raw `stiffness` / `damping` / `mass`. This is the single\n * place the mapping lives; resolvers, value hooks, and any future surface\n * that needs a Reanimated spring config import from here.\n *\n * The mapping is identity (tension ≡ stiffness, friction ≡ damping) — the\n * names differ but the underlying physics constants are the same. We don't\n * surface the raw names publicly because the react-spring vocabulary is\n * what designers and prior-art consumers expect.\n */\nexport function springToReanimated(t: SpringTransition) {\n 'worklet'\n return {\n stiffness: t.tension ?? DEFAULT_SPRING.tension,\n damping: t.friction ?? DEFAULT_SPRING.friction,\n mass: t.mass ?? DEFAULT_SPRING.mass,\n velocity: t.velocity,\n restSpeedThreshold: t.restSpeedThreshold,\n restDisplacementThreshold: t.restDisplacementThreshold,\n }\n}\n","import {\n Easing,\n withDecay,\n withSpring,\n withTiming,\n} from 'react-native-reanimated'\nimport { springToReanimated } from './spring'\nimport { type TransitionConfig } from '../types'\n\nconst DEFAULT_TIMING_DURATION = 250\n\n/**\n * Worklet-safe single-step animation builder. Mirrors a subset of\n * `resolveTransition` for the UI-thread path where the transition config is\n * picked at gesture-release time, not at render time.\n *\n * Supported: spring / timing / decay / no-animation, single-step only.\n * Not supported: sequences, top-level repeat, easing-function\n * auto-worklet-wrapping (pass an already-worklet easing if you need a custom\n * one — most release transitions don't).\n *\n * Use this from gesture worklets (`useDrag` / `usePan` release callbacks, or\n * any custom `Gesture.Pan().onEnd(() => ...)` worklet) to animate a shared\n * value with an Inertia transition without the JS round-trip that would lose\n * the release velocity.\n *\n * For decay transitions, `toValue` is ignored — decay decelerates from the\n * SV's current position via its own physics. Pass `0` if you don't have one.\n */\nexport function buildReleaseAnimation(\n transition: TransitionConfig,\n toValue: number,\n): unknown {\n 'worklet'\n if (transition.type === 'no-animation') return toValue\n if (transition.type === 'decay') {\n const cfg: {\n velocity: number\n deceleration?: number\n clamp?: [number, number]\n } = { velocity: transition.velocity ?? 0 }\n if (transition.deceleration !== undefined) {\n cfg.deceleration = transition.deceleration\n }\n if (transition.clamp !== undefined) cfg.clamp = transition.clamp\n return withDecay(cfg)\n }\n if (transition.type === 'timing') {\n // Reanimated 4's `Easing.bezier(...)` returns an `EasingFunctionFactory`\n // rather than the function itself. Unwrap inline so consumers calling\n // `buildReleaseAnimation` from a gesture worklet don't have to.\n const e = transition.easing\n const easingFn =\n e && typeof e === 'object' && 'factory' in e\n ? e.factory()\n : (e ?? Easing.inOut(Easing.ease))\n return withTiming(toValue, {\n duration: transition.duration ?? DEFAULT_TIMING_DURATION,\n easing: easingFn,\n })\n }\n return withSpring(toValue, springToReanimated(transition))\n}\n","import { useMemo } from 'react'\nimport {\n PanResponder,\n type PanResponderGestureState,\n type PanResponderInstance,\n} from 'react-native'\nimport {\n useAnimatedStyle,\n useSharedValue,\n type SharedValue,\n} from 'react-native-reanimated'\nimport { buildReleaseAnimation } from '../transitions'\nimport type { TransitionConfig } from '../types'\n\n/**\n * Same drag-result shape as `useDrag` from `@onlynative/inertia-gestures`,\n * minus the `gesture` field (PanResponder spreads handlers, no\n * `<GestureDetector>` wrapper). The shared values + animatedStyle are\n * interchangeable across both hooks; consumers can swap implementations\n * without touching their `useAnimatedStyle` consumers.\n */\nexport interface UseTouchDragResult {\n /** Spread onto a `View` / `Pressable` to install the pan responder. */\n panHandlers: PanResponderInstance['panHandlers']\n /** Stable animated `transform` style. */\n animatedStyle: ReturnType<typeof useAnimatedStyle>\n /** Live x translation, persistent across gestures. */\n dragX: SharedValue<number>\n /** Live y translation, persistent across gestures. */\n dragY: SharedValue<number>\n /** True while the gesture is active. */\n isDragging: SharedValue<boolean>\n}\n\n/**\n * Release transition shape for PanResponder's JS-thread `onRelease`. Mirrors\n * the gesture-handler adapter's `ReleaseTransition` but with `to` typed as\n * required for spring/timing/no-animation (decay omits it).\n */\nexport type TouchReleaseTransition =\n | (TransitionConfig & { type: 'spring'; to: number })\n | (TransitionConfig & { type: 'timing'; to: number })\n | (TransitionConfig & { type: 'decay' })\n | (TransitionConfig & { type: 'no-animation'; to: number })\n\nexport interface TouchReleaseInfo {\n x: number\n y: number\n velocity: { x: number; y: number }\n}\n\nexport interface TouchReleaseResult {\n x?: TouchReleaseTransition\n y?: TouchReleaseTransition\n}\n\nexport interface UseTouchDragOptions {\n /**\n * Restrict the drag to one axis. Defaults to `'both'`. When `'x'` is set\n * the y-axis shared value never updates (and vice versa); velocity is\n * still reported on both for `onDragEnd`.\n */\n axis?: 'x' | 'y' | 'both'\n /**\n * Travel bounds (px from resting). Each side is independently optional.\n * Out-of-bounds values clamp to the limit unless `elastic > 0`.\n */\n constraints?: {\n left?: number\n right?: number\n top?: number\n bottom?: number\n }\n /**\n * Rubber-band coefficient applied to overshoot past `constraints`. `0`\n * (default) hard-clamps; `0.2`-`0.4` is a typical Framer-Motion feel.\n */\n elastic?: number\n /**\n * Fires when the user starts dragging. JS thread.\n */\n onDragStart?: () => void\n /**\n * Fires when the user releases or the gesture terminates. JS thread.\n *\n * Velocity is in px/sec to match the `@onlynative/inertia-gestures` API\n * (PanResponder's native `vx` / `vy` are px/ms; the hook normalizes).\n */\n onDragEnd?: (info: TouchReleaseInfo) => void\n /**\n * Optional release-animation callback. Return per-axis release transitions\n * to animate the SVs to a settled position via Inertia's transition\n * resolver — spring snap-to-tick, decay with bounds, timing settle.\n *\n * Unlike the gesture-handler version, this callback runs on the **JS\n * thread** (PanResponder is JS-only). The returned transitions still drive\n * UI-thread animations via Reanimated — only the decision logic is JS-side.\n */\n onRelease?: (info: TouchReleaseInfo) => TouchReleaseResult | void\n}\n\n/**\n * PanResponder-backed drag hook. Pointer-equivalent of `useDrag` from\n * `@onlynative/inertia-gestures`, with two differences:\n *\n * 1. No `react-native-gesture-handler` peer dep required — PanResponder is\n * built into React Native, so this lives in core.\n * 2. Returns `panHandlers` to spread on a `View` / `Pressable` instead of\n * a `gesture` to plug into `<GestureDetector>`.\n *\n * Use this when:\n * - You need keyboard a11y alongside drag (a slider with arrow-key step,\n * a scrollbar with `PageUp` / `PageDown`). PanResponder composes\n * cleanly with `onKeyDown`; gesture-handler doesn't surface keyboard.\n * - You don't want to take `react-native-gesture-handler` as a dependency\n * (smaller bundle, simpler install).\n *\n * Skip this when:\n * - You're already using `react-native-gesture-handler` elsewhere (use\n * `useDrag` from `@onlynative/inertia-gestures` for consistency and\n * better worklet-thread fidelity on release velocity).\n * - You need momentum semantics like the gesture-handler `usePan` —\n * PanResponder's release velocity is JS-thread and slightly less precise.\n *\n * @example\n * ```tsx\n * import { useTouchDrag } from '@onlynative/inertia/touch'\n *\n * function Slider({ ticks }: { ticks: number[] }) {\n * const drag = useTouchDrag({\n * axis: 'x',\n * constraints: { left: 0, right: 280 },\n * onRelease: (e) => {\n * const snap = nearestTick(e.x, ticks)\n * return { x: { type: 'spring', to: snap, velocity: e.velocity.x } }\n * },\n * })\n *\n * return (\n * <Motion.View\n * style={[styles.thumb, drag.animatedStyle]}\n * {...drag.panHandlers}\n * />\n * )\n * }\n * ```\n */\nexport function useTouchDrag(\n options: UseTouchDragOptions = {},\n): UseTouchDragResult {\n const { axis = 'both', constraints, elastic = 0 } = options\n\n const dragX = useSharedValue(0)\n const dragY = useSharedValue(0)\n const startX = useSharedValue(0)\n const startY = useSharedValue(0)\n const isDragging = useSharedValue(false)\n\n // Snapshot scalars into local consts so the responder callbacks close over\n // primitives, not the `options` literal — a fresh `options` each render\n // would otherwise force the PanResponder identity to change.\n const lockX = axis !== 'y'\n const lockY = axis !== 'x'\n const left = constraints?.left\n const right = constraints?.right\n const top = constraints?.top\n const bottom = constraints?.bottom\n const elasticCoef = elastic\n const { onDragStart, onDragEnd, onRelease } = options\n\n const responder = useMemo(\n () => buildResponder(),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [\n lockX,\n lockY,\n left,\n right,\n top,\n bottom,\n elasticCoef,\n onDragStart,\n onDragEnd,\n onRelease,\n ],\n )\n\n // Hoisted out of the inline `useMemo` factory to keep the dep list readable\n // and avoid re-declaring closure helpers each render.\n function buildResponder(): PanResponderInstance {\n const handleEnd = (g: PanResponderGestureState) => {\n isDragging.value = false\n const x = dragX.value\n const y = dragY.value\n // PanResponder velocity is px/ms; multiply to match the\n // `@onlynative/inertia-gestures` API (px/sec from gesture-handler).\n const vx = g.vx * 1000\n const vy = g.vy * 1000\n if (onRelease) {\n const result = onRelease({ x, y, velocity: { x: vx, y: vy } })\n if (result) {\n if (result.x && lockX) {\n const toX = 'to' in result.x ? result.x.to : x\n dragX.value = buildReleaseAnimation(\n result.x,\n toX,\n ) as unknown as number\n }\n if (result.y && lockY) {\n const toY = 'to' in result.y ? result.y.to : y\n dragY.value = buildReleaseAnimation(\n result.y,\n toY,\n ) as unknown as number\n }\n }\n }\n if (onDragEnd) onDragEnd({ x, y, velocity: { x: vx, y: vy } })\n }\n\n return PanResponder.create({\n // Always claim the start so taps that turn into drags don't slip\n // through to a parent ScrollView. Consumers can compose their own\n // capture predicates by wrapping the returned `panHandlers`.\n onStartShouldSetPanResponder: () => true,\n onMoveShouldSetPanResponder: () => true,\n onPanResponderGrant: () => {\n startX.value = dragX.value\n startY.value = dragY.value\n isDragging.value = true\n if (onDragStart) onDragStart()\n },\n onPanResponderMove: (_e, g) => {\n if (lockX) {\n dragX.value = applyBounds(\n startX.value + g.dx,\n left,\n right,\n elasticCoef,\n )\n }\n if (lockY) {\n dragY.value = applyBounds(\n startY.value + g.dy,\n top,\n bottom,\n elasticCoef,\n )\n }\n },\n onPanResponderRelease: (_e, g) => handleEnd(g),\n onPanResponderTerminate: (_e, g) => handleEnd(g),\n })\n }\n\n const animatedStyle = useAnimatedStyle(() => ({\n transform: [{ translateX: dragX.value }, { translateY: dragY.value }],\n }))\n\n return {\n panHandlers: responder.panHandlers,\n animatedStyle,\n dragX,\n dragY,\n isDragging,\n }\n}\n\n/**\n * Clamp `value` to `[min, max]`. When `elastic > 0` the overshoot past a\n * bound is scaled by `elastic`, giving a rubber-band feel. `min` / `max`\n * may be `undefined` to leave that side unbounded.\n *\n * JS-thread (PanResponder callbacks are JS, not worklets).\n */\nfunction applyBounds(\n value: number,\n min: number | undefined,\n max: number | undefined,\n elastic: number,\n): number {\n if (min !== undefined && value < min) {\n return elastic > 0 ? min + (value - min) * elastic : min\n }\n if (max !== undefined && value > max) {\n return elastic > 0 ? max + (value - max) * elastic : max\n }\n return value\n}\n"]}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { PanResponder } from 'react-native';
|
|
3
|
+
import { useSharedValue, useAnimatedStyle, withDecay, Easing, withTiming, withSpring } from 'react-native-reanimated';
|
|
4
|
+
|
|
5
|
+
// src/touch/useTouchDrag.ts
|
|
6
|
+
|
|
7
|
+
// src/transitions/spring.ts
|
|
8
|
+
var DEFAULT_SPRING = {
|
|
9
|
+
tension: 170,
|
|
10
|
+
friction: 26,
|
|
11
|
+
mass: 1
|
|
12
|
+
};
|
|
13
|
+
function springToReanimated(t) {
|
|
14
|
+
"worklet";
|
|
15
|
+
return {
|
|
16
|
+
stiffness: t.tension ?? DEFAULT_SPRING.tension,
|
|
17
|
+
damping: t.friction ?? DEFAULT_SPRING.friction,
|
|
18
|
+
mass: t.mass ?? DEFAULT_SPRING.mass,
|
|
19
|
+
velocity: t.velocity,
|
|
20
|
+
restSpeedThreshold: t.restSpeedThreshold,
|
|
21
|
+
restDisplacementThreshold: t.restDisplacementThreshold
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
var DEFAULT_TIMING_DURATION = 250;
|
|
25
|
+
function buildReleaseAnimation(transition, toValue) {
|
|
26
|
+
"worklet";
|
|
27
|
+
if (transition.type === "no-animation") return toValue;
|
|
28
|
+
if (transition.type === "decay") {
|
|
29
|
+
const cfg = { velocity: transition.velocity ?? 0 };
|
|
30
|
+
if (transition.deceleration !== void 0) {
|
|
31
|
+
cfg.deceleration = transition.deceleration;
|
|
32
|
+
}
|
|
33
|
+
if (transition.clamp !== void 0) cfg.clamp = transition.clamp;
|
|
34
|
+
return withDecay(cfg);
|
|
35
|
+
}
|
|
36
|
+
if (transition.type === "timing") {
|
|
37
|
+
const e = transition.easing;
|
|
38
|
+
const easingFn = e && typeof e === "object" && "factory" in e ? e.factory() : e ?? Easing.inOut(Easing.ease);
|
|
39
|
+
return withTiming(toValue, {
|
|
40
|
+
duration: transition.duration ?? DEFAULT_TIMING_DURATION,
|
|
41
|
+
easing: easingFn
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return withSpring(toValue, springToReanimated(transition));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/touch/useTouchDrag.ts
|
|
48
|
+
function useTouchDrag(options = {}) {
|
|
49
|
+
const { axis = "both", constraints, elastic = 0 } = options;
|
|
50
|
+
const dragX = useSharedValue(0);
|
|
51
|
+
const dragY = useSharedValue(0);
|
|
52
|
+
const startX = useSharedValue(0);
|
|
53
|
+
const startY = useSharedValue(0);
|
|
54
|
+
const isDragging = useSharedValue(false);
|
|
55
|
+
const lockX = axis !== "y";
|
|
56
|
+
const lockY = axis !== "x";
|
|
57
|
+
const left = constraints?.left;
|
|
58
|
+
const right = constraints?.right;
|
|
59
|
+
const top = constraints?.top;
|
|
60
|
+
const bottom = constraints?.bottom;
|
|
61
|
+
const elasticCoef = elastic;
|
|
62
|
+
const { onDragStart, onDragEnd, onRelease } = options;
|
|
63
|
+
const responder = useMemo(
|
|
64
|
+
() => buildResponder(),
|
|
65
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
66
|
+
[
|
|
67
|
+
lockX,
|
|
68
|
+
lockY,
|
|
69
|
+
left,
|
|
70
|
+
right,
|
|
71
|
+
top,
|
|
72
|
+
bottom,
|
|
73
|
+
elasticCoef,
|
|
74
|
+
onDragStart,
|
|
75
|
+
onDragEnd,
|
|
76
|
+
onRelease
|
|
77
|
+
]
|
|
78
|
+
);
|
|
79
|
+
function buildResponder() {
|
|
80
|
+
const handleEnd = (g) => {
|
|
81
|
+
isDragging.value = false;
|
|
82
|
+
const x = dragX.value;
|
|
83
|
+
const y = dragY.value;
|
|
84
|
+
const vx = g.vx * 1e3;
|
|
85
|
+
const vy = g.vy * 1e3;
|
|
86
|
+
if (onRelease) {
|
|
87
|
+
const result = onRelease({ x, y, velocity: { x: vx, y: vy } });
|
|
88
|
+
if (result) {
|
|
89
|
+
if (result.x && lockX) {
|
|
90
|
+
const toX = "to" in result.x ? result.x.to : x;
|
|
91
|
+
dragX.value = buildReleaseAnimation(
|
|
92
|
+
result.x,
|
|
93
|
+
toX
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
if (result.y && lockY) {
|
|
97
|
+
const toY = "to" in result.y ? result.y.to : y;
|
|
98
|
+
dragY.value = buildReleaseAnimation(
|
|
99
|
+
result.y,
|
|
100
|
+
toY
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (onDragEnd) onDragEnd({ x, y, velocity: { x: vx, y: vy } });
|
|
106
|
+
};
|
|
107
|
+
return PanResponder.create({
|
|
108
|
+
// Always claim the start so taps that turn into drags don't slip
|
|
109
|
+
// through to a parent ScrollView. Consumers can compose their own
|
|
110
|
+
// capture predicates by wrapping the returned `panHandlers`.
|
|
111
|
+
onStartShouldSetPanResponder: () => true,
|
|
112
|
+
onMoveShouldSetPanResponder: () => true,
|
|
113
|
+
onPanResponderGrant: () => {
|
|
114
|
+
startX.value = dragX.value;
|
|
115
|
+
startY.value = dragY.value;
|
|
116
|
+
isDragging.value = true;
|
|
117
|
+
if (onDragStart) onDragStart();
|
|
118
|
+
},
|
|
119
|
+
onPanResponderMove: (_e, g) => {
|
|
120
|
+
if (lockX) {
|
|
121
|
+
dragX.value = applyBounds(
|
|
122
|
+
startX.value + g.dx,
|
|
123
|
+
left,
|
|
124
|
+
right,
|
|
125
|
+
elasticCoef
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
if (lockY) {
|
|
129
|
+
dragY.value = applyBounds(
|
|
130
|
+
startY.value + g.dy,
|
|
131
|
+
top,
|
|
132
|
+
bottom,
|
|
133
|
+
elasticCoef
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
onPanResponderRelease: (_e, g) => handleEnd(g),
|
|
138
|
+
onPanResponderTerminate: (_e, g) => handleEnd(g)
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
142
|
+
transform: [{ translateX: dragX.value }, { translateY: dragY.value }]
|
|
143
|
+
}));
|
|
144
|
+
return {
|
|
145
|
+
panHandlers: responder.panHandlers,
|
|
146
|
+
animatedStyle,
|
|
147
|
+
dragX,
|
|
148
|
+
dragY,
|
|
149
|
+
isDragging
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function applyBounds(value, min, max, elastic) {
|
|
153
|
+
if (min !== void 0 && value < min) {
|
|
154
|
+
return elastic > 0 ? min + (value - min) * elastic : min;
|
|
155
|
+
}
|
|
156
|
+
if (max !== void 0 && value > max) {
|
|
157
|
+
return elastic > 0 ? max + (value - max) * elastic : max;
|
|
158
|
+
}
|
|
159
|
+
return value;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export { useTouchDrag };
|
|
163
|
+
//# sourceMappingURL=index.mjs.map
|
|
164
|
+
//# sourceMappingURL=index.mjs.map
|