@mantine/hooks 9.1.1 → 9.2.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/cjs/index.cjs CHANGED
@@ -87,6 +87,7 @@ const require_use_collapse = require("./use-collapse/use-collapse.cjs");
87
87
  const require_use_horizontal_collapse = require("./use-collapse/use-horizontal-collapse.cjs");
88
88
  const require_use_mask = require("./use-mask/use-mask.cjs");
89
89
  const require_use_roving_index = require("./use-roving-index/use-roving-index.cjs");
90
+ const require_use_drag = require("./use-drag/use-drag.cjs");
90
91
  exports.assignRef = require_use_merged_ref.assignRef;
91
92
  exports.clamp = require_clamp.clamp;
92
93
  exports.clampUseMovePosition = require_use_move.clampUseMovePosition;
@@ -117,6 +118,7 @@ exports.useDidUpdate = require_use_did_update.useDidUpdate;
117
118
  exports.useDisclosure = require_use_disclosure.useDisclosure;
118
119
  exports.useDocumentTitle = require_use_document_title.useDocumentTitle;
119
120
  exports.useDocumentVisibility = require_use_document_visibility.useDocumentVisibility;
121
+ exports.useDrag = require_use_drag.useDrag;
120
122
  exports.useElementSize = require_use_resize_observer.useElementSize;
121
123
  exports.useEventListener = require_use_event_listener.useEventListener;
122
124
  exports.useEyeDropper = require_use_eye_dropper.useEyeDropper;
@@ -0,0 +1,266 @@
1
+ "use client";
2
+ let react = require("react");
3
+ //#region packages/@mantine/hooks/src/use-drag/use-drag.ts
4
+ const VELOCITY_DECAY_MS = 100;
5
+ function sign(n) {
6
+ if (n > 0) return 1;
7
+ if (n < 0) return -1;
8
+ return 0;
9
+ }
10
+ function getThresholdVector(threshold) {
11
+ const t = threshold ?? 0;
12
+ if (typeof t === "number") return [t, t];
13
+ return t;
14
+ }
15
+ function createInitialState() {
16
+ return {
17
+ isActive: false,
18
+ pointerId: -1,
19
+ startXY: [0, 0],
20
+ prevXY: [0, 0],
21
+ startTimestamp: 0,
22
+ prevTimestamp: 0,
23
+ thresholdMet: false,
24
+ firstFired: false,
25
+ lockedAxis: null,
26
+ canceled: false,
27
+ lastVelocity: [0, 0]
28
+ };
29
+ }
30
+ function useDrag(handler, options = {}) {
31
+ const [active, setActive] = (0, react.useState)(false);
32
+ const handlerRef = (0, react.useRef)(handler);
33
+ handlerRef.current = handler;
34
+ const optionsRef = (0, react.useRef)(options);
35
+ optionsRef.current = options;
36
+ const stateRef = (0, react.useRef)(createInitialState());
37
+ const documentControllerRef = (0, react.useRef)(null);
38
+ return {
39
+ ref: (0, react.useCallback)((node) => {
40
+ if (!node) return;
41
+ const elementController = new AbortController();
42
+ const applyAxisConstraint = (v) => {
43
+ const opts = optionsRef.current;
44
+ const s = stateRef.current;
45
+ if (opts.axis === "x") return [v[0], 0];
46
+ if (opts.axis === "y") return [0, v[1]];
47
+ if (opts.axis === "lock") {
48
+ if (s.lockedAxis === null) {
49
+ const t = opts.axisThreshold ?? 1;
50
+ if (Math.abs(v[0]) > t || Math.abs(v[1]) > t) s.lockedAxis = Math.abs(v[0]) >= Math.abs(v[1]) ? "x" : "y";
51
+ }
52
+ if (s.lockedAxis === "x") return [v[0], 0];
53
+ if (s.lockedAxis === "y") return [0, v[1]];
54
+ }
55
+ return v;
56
+ };
57
+ const resetDrag = () => {
58
+ const s = stateRef.current;
59
+ s.isActive = false;
60
+ s.pointerId = -1;
61
+ s.thresholdMet = false;
62
+ s.firstFired = false;
63
+ s.lockedAxis = null;
64
+ s.canceled = false;
65
+ setActive(false);
66
+ document.body.style.userSelect = "";
67
+ document.body.style.webkitUserSelect = "";
68
+ documentControllerRef.current?.abort();
69
+ documentControllerRef.current = null;
70
+ };
71
+ const cancel = () => {
72
+ if (stateRef.current.isActive) {
73
+ stateRef.current.canceled = true;
74
+ resetDrag();
75
+ }
76
+ };
77
+ const activateDrag = () => {
78
+ setActive(true);
79
+ document.body.style.userSelect = "none";
80
+ document.body.style.webkitUserSelect = "none";
81
+ };
82
+ const onPointerDown = (event) => {
83
+ if (optionsRef.current.enabled === false) return;
84
+ if (event.button !== 0) return;
85
+ if (stateRef.current.isActive) return;
86
+ const s = stateRef.current;
87
+ s.isActive = true;
88
+ s.pointerId = event.pointerId;
89
+ s.startXY = [event.clientX, event.clientY];
90
+ s.prevXY = [event.clientX, event.clientY];
91
+ s.startTimestamp = event.timeStamp;
92
+ s.prevTimestamp = event.timeStamp;
93
+ s.thresholdMet = false;
94
+ s.firstFired = false;
95
+ s.lockedAxis = null;
96
+ s.canceled = false;
97
+ s.lastVelocity = [0, 0];
98
+ const [tx, ty] = getThresholdVector(optionsRef.current.threshold);
99
+ if (tx === 0 && ty === 0) {
100
+ s.thresholdMet = true;
101
+ s.firstFired = true;
102
+ activateDrag();
103
+ handlerRef.current({
104
+ xy: [event.clientX, event.clientY],
105
+ initial: [event.clientX, event.clientY],
106
+ movement: [0, 0],
107
+ delta: [0, 0],
108
+ distance: [0, 0],
109
+ direction: [0, 0],
110
+ velocity: [0, 0],
111
+ elapsedTime: 0,
112
+ first: true,
113
+ last: false,
114
+ active: true,
115
+ tap: false,
116
+ canceled: false,
117
+ cancel,
118
+ event
119
+ });
120
+ }
121
+ documentControllerRef.current?.abort();
122
+ documentControllerRef.current = new AbortController();
123
+ const sig = documentControllerRef.current.signal;
124
+ document.addEventListener("pointermove", onPointerMove, { signal: sig });
125
+ document.addEventListener("pointerup", onPointerUp, { signal: sig });
126
+ document.addEventListener("pointercancel", onPointerCancel, { signal: sig });
127
+ };
128
+ const onPointerMove = (event) => {
129
+ const s = stateRef.current;
130
+ if (!s.isActive || event.pointerId !== s.pointerId) return;
131
+ const rawMovement = [event.clientX - s.startXY[0], event.clientY - s.startXY[1]];
132
+ if (!s.thresholdMet) {
133
+ const [tx, ty] = getThresholdVector(optionsRef.current.threshold);
134
+ if (Math.abs(rawMovement[0]) < tx && Math.abs(rawMovement[1]) < ty) {
135
+ s.prevXY = [event.clientX, event.clientY];
136
+ s.prevTimestamp = event.timeStamp;
137
+ return;
138
+ }
139
+ s.thresholdMet = true;
140
+ activateDrag();
141
+ }
142
+ const movement = applyAxisConstraint(rawMovement);
143
+ const delta = applyAxisConstraint([event.clientX - s.prevXY[0], event.clientY - s.prevXY[1]]);
144
+ const timeDelta = event.timeStamp - s.prevTimestamp;
145
+ const velocity = timeDelta > 0 ? [Math.abs(delta[0]) / timeDelta, Math.abs(delta[1]) / timeDelta] : s.lastVelocity;
146
+ s.lastVelocity = velocity;
147
+ const isFirst = !s.firstFired;
148
+ s.firstFired = true;
149
+ s.prevXY = [event.clientX, event.clientY];
150
+ s.prevTimestamp = event.timeStamp;
151
+ handlerRef.current({
152
+ xy: [event.clientX, event.clientY],
153
+ initial: [...s.startXY],
154
+ movement,
155
+ delta,
156
+ distance: [Math.abs(movement[0]), Math.abs(movement[1])],
157
+ direction: [sign(delta[0]), sign(delta[1])],
158
+ velocity,
159
+ elapsedTime: event.timeStamp - s.startTimestamp,
160
+ first: isFirst,
161
+ last: false,
162
+ active: true,
163
+ tap: false,
164
+ canceled: false,
165
+ cancel,
166
+ event
167
+ });
168
+ };
169
+ const onPointerUp = (event) => {
170
+ const s = stateRef.current;
171
+ if (!s.isActive || event.pointerId !== s.pointerId) return;
172
+ const opts = optionsRef.current;
173
+ if (!s.thresholdMet) {
174
+ if (opts.filterTaps) {
175
+ const mov = applyAxisConstraint([event.clientX - s.startXY[0], event.clientY - s.startXY[1]]);
176
+ const dist = [Math.abs(mov[0]), Math.abs(mov[1])];
177
+ const isTap = Math.max(dist[0], dist[1]) < (opts.tapThreshold ?? 3);
178
+ handlerRef.current({
179
+ xy: [event.clientX, event.clientY],
180
+ initial: [...s.startXY],
181
+ movement: mov,
182
+ delta: mov,
183
+ distance: dist,
184
+ direction: [sign(mov[0]), sign(mov[1])],
185
+ velocity: [0, 0],
186
+ elapsedTime: event.timeStamp - s.startTimestamp,
187
+ first: true,
188
+ last: true,
189
+ active: false,
190
+ tap: isTap,
191
+ canceled: false,
192
+ cancel,
193
+ event
194
+ });
195
+ }
196
+ resetDrag();
197
+ return;
198
+ }
199
+ const movement = applyAxisConstraint([event.clientX - s.startXY[0], event.clientY - s.startXY[1]]);
200
+ const distance = [Math.abs(movement[0]), Math.abs(movement[1])];
201
+ const delta = applyAxisConstraint([event.clientX - s.prevXY[0], event.clientY - s.prevXY[1]]);
202
+ const velocity = event.timeStamp - s.prevTimestamp > VELOCITY_DECAY_MS ? [0, 0] : s.lastVelocity;
203
+ const maxDistance = Math.max(distance[0], distance[1]);
204
+ const tap = opts.filterTaps === true && maxDistance < (opts.tapThreshold ?? 3);
205
+ handlerRef.current({
206
+ xy: [event.clientX, event.clientY],
207
+ initial: [...s.startXY],
208
+ movement,
209
+ delta,
210
+ distance,
211
+ direction: [sign(delta[0]), sign(delta[1])],
212
+ velocity,
213
+ elapsedTime: event.timeStamp - s.startTimestamp,
214
+ first: !s.firstFired,
215
+ last: true,
216
+ active: false,
217
+ tap,
218
+ canceled: false,
219
+ cancel,
220
+ event
221
+ });
222
+ resetDrag();
223
+ };
224
+ const onPointerCancel = (event) => {
225
+ const s = stateRef.current;
226
+ if (!s.isActive || event.pointerId !== s.pointerId) return;
227
+ const movement = applyAxisConstraint([event.clientX - s.startXY[0], event.clientY - s.startXY[1]]);
228
+ handlerRef.current({
229
+ xy: [event.clientX, event.clientY],
230
+ initial: [...s.startXY],
231
+ movement,
232
+ delta: [0, 0],
233
+ distance: [Math.abs(movement[0]), Math.abs(movement[1])],
234
+ direction: [0, 0],
235
+ velocity: [0, 0],
236
+ elapsedTime: event.timeStamp - s.startTimestamp,
237
+ first: !s.firstFired,
238
+ last: true,
239
+ active: false,
240
+ tap: false,
241
+ canceled: true,
242
+ cancel,
243
+ event
244
+ });
245
+ resetDrag();
246
+ };
247
+ node.addEventListener("pointerdown", onPointerDown, { signal: elementController.signal });
248
+ return () => {
249
+ elementController.abort();
250
+ documentControllerRef.current?.abort();
251
+ documentControllerRef.current = null;
252
+ if (stateRef.current.isActive) {
253
+ stateRef.current.isActive = false;
254
+ setActive(false);
255
+ document.body.style.userSelect = "";
256
+ document.body.style.webkitUserSelect = "";
257
+ }
258
+ };
259
+ }, []),
260
+ active
261
+ };
262
+ }
263
+ //#endregion
264
+ exports.useDrag = useDrag;
265
+
266
+ //# sourceMappingURL=use-drag.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-drag.cjs","names":[],"sources":["../../src/use-drag/use-drag.ts"],"sourcesContent":["// Required to disable for webkit-user-select, although deprecated, it is still required for Safari support\n/* eslint-disable @typescript-eslint/no-deprecated */\nimport { useCallback, useRef, useState } from 'react';\n\ntype Vector2 = [number, number];\n\nexport interface UseDragState {\n /** Current pointer position [x, y] */\n xy: Vector2;\n\n /** Position where the gesture started [x, y] */\n initial: Vector2;\n\n /** Displacement from start [x, y], respects axis constraint */\n movement: Vector2;\n\n /** Change since previous event [x, y] */\n delta: Vector2;\n\n /** Absolute distance per axis [x, y] */\n distance: Vector2;\n\n /** Movement direction per axis: -1, 0, or 1 */\n direction: Vector2;\n\n /** Speed per axis in px/ms */\n velocity: Vector2;\n\n /** Time since drag started in ms */\n elapsedTime: number;\n\n /** `true` on the first handler call after the threshold is met */\n first: boolean;\n\n /** `true` on the last handler call (pointer released or canceled) */\n last: boolean;\n\n /** `true` while the gesture is ongoing */\n active: boolean;\n\n /** `true` when the gesture qualifies as a tap (requires `filterTaps: true`) */\n tap: boolean;\n\n /** `true` when the gesture was interrupted by a `pointercancel` event */\n canceled: boolean;\n\n /** Function to programmatically cancel the current gesture */\n cancel: () => void;\n\n /** The source `PointerEvent` */\n event: PointerEvent;\n}\n\nexport interface UseDragOptions {\n /** Constrain movement to a specific axis. `'lock'` locks to whichever axis has more movement after `axisThreshold` is exceeded. */\n axis?: 'x' | 'y' | 'lock';\n\n /** Movement in px required to determine axis when `axis` is `'lock'`, `1` by default */\n axisThreshold?: number;\n\n /** When `true`, the last state includes `tap: true` when total distance is below `tapThreshold`, `false` by default */\n filterTaps?: boolean;\n\n /** Max displacement in px to still be considered a tap, `3` by default */\n tapThreshold?: number;\n\n /** Minimum displacement in px before the drag activates. Can be a number (both axes) or `[x, y]`. `0` by default */\n threshold?: number | Vector2;\n\n /** Enable or disable the hook, `true` by default */\n enabled?: boolean;\n}\n\nexport interface UseDragReturnValue<T extends HTMLElement = any> {\n /** Ref callback to attach to the draggable element */\n ref: React.RefCallback<T | null>;\n\n /** `true` while the element is being dragged */\n active: boolean;\n}\n\nconst VELOCITY_DECAY_MS = 100;\n\nfunction sign(n: number): -1 | 0 | 1 {\n if (n > 0) {\n return 1;\n }\n if (n < 0) {\n return -1;\n }\n return 0;\n}\n\nfunction getThresholdVector(threshold: number | Vector2 | undefined): Vector2 {\n const t = threshold ?? 0;\n if (typeof t === 'number') {\n return [t, t];\n }\n return t;\n}\n\ninterface DragInternalState {\n isActive: boolean;\n pointerId: number;\n startXY: Vector2;\n prevXY: Vector2;\n startTimestamp: number;\n prevTimestamp: number;\n thresholdMet: boolean;\n firstFired: boolean;\n lockedAxis: 'x' | 'y' | null;\n canceled: boolean;\n lastVelocity: Vector2;\n}\n\nfunction createInitialState(): DragInternalState {\n return {\n isActive: false,\n pointerId: -1,\n startXY: [0, 0],\n prevXY: [0, 0],\n startTimestamp: 0,\n prevTimestamp: 0,\n thresholdMet: false,\n firstFired: false,\n lockedAxis: null,\n canceled: false,\n lastVelocity: [0, 0],\n };\n}\n\nexport function useDrag<T extends HTMLElement = any>(\n handler: (state: UseDragState) => void,\n options: UseDragOptions = {}\n): UseDragReturnValue<T> {\n const [active, setActive] = useState(false);\n\n const handlerRef = useRef(handler);\n handlerRef.current = handler;\n\n const optionsRef = useRef(options);\n optionsRef.current = options;\n\n const stateRef = useRef<DragInternalState>(createInitialState());\n const documentControllerRef = useRef<AbortController | null>(null);\n\n const refCallback: React.RefCallback<T | null> = useCallback((node) => {\n if (!node) {\n return undefined;\n }\n\n const elementController = new AbortController();\n\n const applyAxisConstraint = (v: Vector2): Vector2 => {\n const opts = optionsRef.current;\n const s = stateRef.current;\n\n if (opts.axis === 'x') {\n return [v[0], 0];\n }\n if (opts.axis === 'y') {\n return [0, v[1]];\n }\n if (opts.axis === 'lock') {\n if (s.lockedAxis === null) {\n const t = opts.axisThreshold ?? 1;\n if (Math.abs(v[0]) > t || Math.abs(v[1]) > t) {\n s.lockedAxis = Math.abs(v[0]) >= Math.abs(v[1]) ? 'x' : 'y';\n }\n }\n if (s.lockedAxis === 'x') {\n return [v[0], 0];\n }\n if (s.lockedAxis === 'y') {\n return [0, v[1]];\n }\n }\n return v;\n };\n\n const resetDrag = () => {\n const s = stateRef.current;\n s.isActive = false;\n s.pointerId = -1;\n s.thresholdMet = false;\n s.firstFired = false;\n s.lockedAxis = null;\n s.canceled = false;\n setActive(false);\n document.body.style.userSelect = '';\n document.body.style.webkitUserSelect = '';\n documentControllerRef.current?.abort();\n documentControllerRef.current = null;\n };\n\n const cancel = () => {\n if (stateRef.current.isActive) {\n stateRef.current.canceled = true;\n resetDrag();\n }\n };\n\n const activateDrag = () => {\n setActive(true);\n document.body.style.userSelect = 'none';\n document.body.style.webkitUserSelect = 'none';\n };\n\n const onPointerDown = (event: PointerEvent) => {\n if (optionsRef.current.enabled === false) {\n return;\n }\n if (event.button !== 0) {\n return;\n }\n if (stateRef.current.isActive) {\n return;\n }\n\n const s = stateRef.current;\n s.isActive = true;\n s.pointerId = event.pointerId;\n s.startXY = [event.clientX, event.clientY];\n s.prevXY = [event.clientX, event.clientY];\n s.startTimestamp = event.timeStamp;\n s.prevTimestamp = event.timeStamp;\n s.thresholdMet = false;\n s.firstFired = false;\n s.lockedAxis = null;\n s.canceled = false;\n s.lastVelocity = [0, 0];\n\n const [tx, ty] = getThresholdVector(optionsRef.current.threshold);\n if (tx === 0 && ty === 0) {\n s.thresholdMet = true;\n s.firstFired = true;\n activateDrag();\n\n handlerRef.current({\n xy: [event.clientX, event.clientY],\n initial: [event.clientX, event.clientY],\n movement: [0, 0],\n delta: [0, 0],\n distance: [0, 0],\n direction: [0, 0],\n velocity: [0, 0],\n elapsedTime: 0,\n first: true,\n last: false,\n active: true,\n tap: false,\n canceled: false,\n cancel,\n event,\n });\n }\n\n documentControllerRef.current?.abort();\n documentControllerRef.current = new AbortController();\n const sig = documentControllerRef.current.signal;\n\n document.addEventListener('pointermove', onPointerMove, { signal: sig });\n document.addEventListener('pointerup', onPointerUp, { signal: sig });\n document.addEventListener('pointercancel', onPointerCancel, { signal: sig });\n };\n\n const onPointerMove = (event: PointerEvent) => {\n const s = stateRef.current;\n if (!s.isActive || event.pointerId !== s.pointerId) {\n return;\n }\n\n const rawMovement: Vector2 = [event.clientX - s.startXY[0], event.clientY - s.startXY[1]];\n\n if (!s.thresholdMet) {\n const [tx, ty] = getThresholdVector(optionsRef.current.threshold);\n if (Math.abs(rawMovement[0]) < tx && Math.abs(rawMovement[1]) < ty) {\n s.prevXY = [event.clientX, event.clientY];\n s.prevTimestamp = event.timeStamp;\n return;\n }\n s.thresholdMet = true;\n activateDrag();\n }\n\n const movement = applyAxisConstraint(rawMovement);\n const rawDelta: Vector2 = [event.clientX - s.prevXY[0], event.clientY - s.prevXY[1]];\n const delta = applyAxisConstraint(rawDelta);\n const timeDelta = event.timeStamp - s.prevTimestamp;\n const velocity: Vector2 =\n timeDelta > 0\n ? [Math.abs(delta[0]) / timeDelta, Math.abs(delta[1]) / timeDelta]\n : s.lastVelocity;\n\n s.lastVelocity = velocity;\n const isFirst = !s.firstFired;\n s.firstFired = true;\n s.prevXY = [event.clientX, event.clientY];\n s.prevTimestamp = event.timeStamp;\n\n handlerRef.current({\n xy: [event.clientX, event.clientY],\n initial: [...s.startXY],\n movement,\n delta,\n distance: [Math.abs(movement[0]), Math.abs(movement[1])],\n direction: [sign(delta[0]), sign(delta[1])],\n velocity,\n elapsedTime: event.timeStamp - s.startTimestamp,\n first: isFirst,\n last: false,\n active: true,\n tap: false,\n canceled: false,\n cancel,\n event,\n });\n };\n\n const onPointerUp = (event: PointerEvent) => {\n const s = stateRef.current;\n if (!s.isActive || event.pointerId !== s.pointerId) {\n return;\n }\n\n const opts = optionsRef.current;\n\n if (!s.thresholdMet) {\n if (opts.filterTaps) {\n const rawMov: Vector2 = [event.clientX - s.startXY[0], event.clientY - s.startXY[1]];\n const mov = applyAxisConstraint(rawMov);\n const dist: Vector2 = [Math.abs(mov[0]), Math.abs(mov[1])];\n const maxDist = Math.max(dist[0], dist[1]);\n const isTap = maxDist < (opts.tapThreshold ?? 3);\n\n handlerRef.current({\n xy: [event.clientX, event.clientY],\n initial: [...s.startXY],\n movement: mov,\n delta: mov,\n distance: dist,\n direction: [sign(mov[0]), sign(mov[1])],\n velocity: [0, 0],\n elapsedTime: event.timeStamp - s.startTimestamp,\n first: true,\n last: true,\n active: false,\n tap: isTap,\n canceled: false,\n cancel,\n event,\n });\n }\n resetDrag();\n return;\n }\n\n const rawMovement: Vector2 = [event.clientX - s.startXY[0], event.clientY - s.startXY[1]];\n const movement = applyAxisConstraint(rawMovement);\n const distance: Vector2 = [Math.abs(movement[0]), Math.abs(movement[1])];\n const rawDelta: Vector2 = [event.clientX - s.prevXY[0], event.clientY - s.prevXY[1]];\n const delta = applyAxisConstraint(rawDelta);\n\n const timeSinceLastMove = event.timeStamp - s.prevTimestamp;\n const velocity: Vector2 = timeSinceLastMove > VELOCITY_DECAY_MS ? [0, 0] : s.lastVelocity;\n\n const maxDistance = Math.max(distance[0], distance[1]);\n const tap = opts.filterTaps === true && maxDistance < (opts.tapThreshold ?? 3);\n\n handlerRef.current({\n xy: [event.clientX, event.clientY],\n initial: [...s.startXY],\n movement,\n delta,\n distance,\n direction: [sign(delta[0]), sign(delta[1])],\n velocity,\n elapsedTime: event.timeStamp - s.startTimestamp,\n first: !s.firstFired,\n last: true,\n active: false,\n tap,\n canceled: false,\n cancel,\n event,\n });\n\n resetDrag();\n };\n\n const onPointerCancel = (event: PointerEvent) => {\n const s = stateRef.current;\n if (!s.isActive || event.pointerId !== s.pointerId) {\n return;\n }\n\n const rawMovement: Vector2 = [event.clientX - s.startXY[0], event.clientY - s.startXY[1]];\n const movement = applyAxisConstraint(rawMovement);\n\n handlerRef.current({\n xy: [event.clientX, event.clientY],\n initial: [...s.startXY],\n movement,\n delta: [0, 0],\n distance: [Math.abs(movement[0]), Math.abs(movement[1])],\n direction: [0, 0],\n velocity: [0, 0],\n elapsedTime: event.timeStamp - s.startTimestamp,\n first: !s.firstFired,\n last: true,\n active: false,\n tap: false,\n canceled: true,\n cancel,\n event,\n });\n\n resetDrag();\n };\n\n node.addEventListener('pointerdown', onPointerDown, {\n signal: elementController.signal,\n });\n\n return () => {\n elementController.abort();\n documentControllerRef.current?.abort();\n documentControllerRef.current = null;\n if (stateRef.current.isActive) {\n stateRef.current.isActive = false;\n setActive(false);\n document.body.style.userSelect = '';\n document.body.style.webkitUserSelect = '';\n }\n };\n }, []);\n\n return { ref: refCallback, active };\n}\n\nexport namespace useDrag {\n export type State = UseDragState;\n export type Options = UseDragOptions;\n export type ReturnValue<T extends HTMLElement = any> = UseDragReturnValue<T>;\n}\n"],"mappings":"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"}
@@ -29,6 +29,12 @@ function normalizeRadialValue(degree, step) {
29
29
  }
30
30
  function useRadialMove(onChange, { step = .01, onChangeEnd, onScrubStart, onScrubEnd } = {}) {
31
31
  const [active, setActive] = (0, react.useState)(false);
32
+ const cleanupRef = (0, react.useRef)(null);
33
+ (0, react.useEffect)(() => {
34
+ return () => {
35
+ cleanupRef.current?.();
36
+ };
37
+ }, []);
32
38
  return {
33
39
  ref: (0, react.useCallback)((node) => {
34
40
  const update = (event, done = false) => {
@@ -81,15 +87,17 @@ function useRadialMove(onChange, { step = .01, onChangeEnd, onScrubStart, onScru
81
87
  };
82
88
  node?.addEventListener("mousedown", onMouseDown);
83
89
  node?.addEventListener("touchstart", handleTouchStart, { passive: false });
90
+ cleanupRef.current = () => {
91
+ document.removeEventListener("mousemove", handleMouseMove, false);
92
+ document.removeEventListener("mouseup", handleMouseUp, false);
93
+ document.removeEventListener("touchmove", handleTouchMove, false);
94
+ document.removeEventListener("touchend", handleTouchEnd, false);
95
+ };
84
96
  return () => {
85
97
  if (node) {
86
98
  node.removeEventListener("mousedown", onMouseDown);
87
99
  node.removeEventListener("touchstart", handleTouchStart);
88
100
  }
89
- document.removeEventListener("mousemove", handleMouseMove, false);
90
- document.removeEventListener("mouseup", handleMouseUp, false);
91
- document.removeEventListener("touchmove", handleTouchMove, false);
92
- document.removeEventListener("touchend", handleTouchEnd, false);
93
101
  };
94
102
  }, [onChange]),
95
103
  active
@@ -1 +1 @@
1
- {"version":3,"file":"use-radial-move.cjs","names":["clamp"],"sources":["../../src/use-radial-move/use-radial-move.ts"],"sourcesContent":["import { useCallback, useState } from 'react';\nimport { clamp } from '../utils';\n\nfunction radiansToDegrees(radians: number) {\n return radians * (180 / Math.PI);\n}\n\nfunction getElementCenter(element: HTMLElement) {\n const rect = element.getBoundingClientRect();\n return [rect.left + rect.width / 2, rect.top + rect.height / 2];\n}\n\nfunction getAngle(coordinates: [number, number], element: HTMLElement) {\n const center = getElementCenter(element);\n const x = coordinates[0] - center[0];\n const y = coordinates[1] - center[1];\n const deg = radiansToDegrees(Math.atan2(x, y)) + 180;\n return 360 - deg;\n}\n\nfunction toFixed(value: number, digits: number) {\n return parseFloat(value.toFixed(digits));\n}\n\nfunction getDigitsAfterDot(value: number) {\n return value.toString().split('.')[1]?.length || 0;\n}\n\nexport function normalizeRadialValue(degree: number, step: number) {\n const clamped = clamp(degree, 0, 360);\n const high = Math.ceil(clamped / step);\n const low = Math.round(clamped / step);\n return toFixed(\n high >= clamped / step ? (high * step === 360 ? 0 : high * step) : low * step,\n getDigitsAfterDot(step)\n );\n}\n\nexport interface UseRadialMoveOptions {\n /** Number by which value is incremented/decremented with mouse and touch events, `0.01` by default */\n step?: number;\n\n /** Called in `onMouseUp` and `onTouchEnd` events with the current value */\n onChangeEnd?: (value: number) => void;\n\n /** Called in `onMouseDown` and `onTouchStart` events */\n onScrubStart?: () => void;\n\n /** Called in `onMouseUp` and `onTouchEnd` events */\n onScrubEnd?: () => void;\n}\n\nexport interface UseRadialMoveReturnValue<T extends HTMLElement = any> {\n /** Ref to be passed to the element that should be used for radial move */\n ref: React.RefCallback<T | null>;\n\n /** Indicates whether the radial move is active */\n active: boolean;\n}\n\nexport function useRadialMove<T extends HTMLElement = any>(\n onChange: (value: number) => void,\n { step = 0.01, onChangeEnd, onScrubStart, onScrubEnd }: UseRadialMoveOptions = {}\n): UseRadialMoveReturnValue<T> {\n const [active, setActive] = useState(false);\n\n const refCallback: React.RefCallback<T | null> = useCallback(\n (node) => {\n const update = (event: MouseEvent, done = false) => {\n if (node) {\n node.style.userSelect = 'none';\n const deg = getAngle([event.clientX, event.clientY], node);\n const newValue = normalizeRadialValue(deg, step || 1);\n\n onChange(newValue);\n done && onChangeEnd?.(newValue);\n }\n };\n\n const beginTracking = () => {\n onScrubStart?.();\n setActive(true);\n document.addEventListener('mousemove', handleMouseMove, false);\n document.addEventListener('mouseup', handleMouseUp, false);\n document.addEventListener('touchmove', handleTouchMove, { passive: false });\n document.addEventListener('touchend', handleTouchEnd, false);\n };\n\n const endTracking = () => {\n onScrubEnd?.();\n setActive(false);\n document.removeEventListener('mousemove', handleMouseMove, false);\n document.removeEventListener('mouseup', handleMouseUp, false);\n document.removeEventListener('touchmove', handleTouchMove, false);\n document.removeEventListener('touchend', handleTouchEnd, false);\n };\n\n const onMouseDown = (event: MouseEvent) => {\n beginTracking();\n update(event);\n };\n\n const handleMouseMove = (event: MouseEvent) => {\n update(event);\n };\n\n const handleMouseUp = (event: MouseEvent) => {\n update(event, true);\n endTracking();\n };\n\n const handleTouchMove = (event: TouchEvent) => {\n event.preventDefault();\n update(event.touches[0] as any);\n };\n\n const handleTouchEnd = (event: TouchEvent) => {\n update(event.changedTouches[0] as any, true);\n endTracking();\n };\n\n const handleTouchStart = (event: TouchEvent) => {\n event.preventDefault();\n beginTracking();\n update(event.touches[0] as any);\n };\n\n node?.addEventListener('mousedown', onMouseDown);\n node?.addEventListener('touchstart', handleTouchStart, { passive: false });\n\n return () => {\n if (node) {\n node.removeEventListener('mousedown', onMouseDown);\n node.removeEventListener('touchstart', handleTouchStart);\n }\n document.removeEventListener('mousemove', handleMouseMove, false);\n document.removeEventListener('mouseup', handleMouseUp, false);\n document.removeEventListener('touchmove', handleTouchMove, false);\n document.removeEventListener('touchend', handleTouchEnd, false);\n };\n },\n [onChange]\n );\n\n return { ref: refCallback, active };\n}\n\nexport namespace useRadialMove {\n export type Options = UseRadialMoveOptions;\n export type ReturnValue<T extends HTMLElement> = UseRadialMoveReturnValue<T>;\n}\n"],"mappings":";;;;AAGA,SAAS,iBAAiB,SAAiB;AACzC,QAAO,WAAW,MAAM,KAAK;;AAG/B,SAAS,iBAAiB,SAAsB;CAC9C,MAAM,OAAO,QAAQ,uBAAuB;AAC5C,QAAO,CAAC,KAAK,OAAO,KAAK,QAAQ,GAAG,KAAK,MAAM,KAAK,SAAS,EAAE;;AAGjE,SAAS,SAAS,aAA+B,SAAsB;CACrE,MAAM,SAAS,iBAAiB,QAAQ;CACxC,MAAM,IAAI,YAAY,KAAK,OAAO;CAClC,MAAM,IAAI,YAAY,KAAK,OAAO;AAElC,QAAO,OADK,iBAAiB,KAAK,MAAM,GAAG,EAAE,CAAC,GAAG;;AAInD,SAAS,QAAQ,OAAe,QAAgB;AAC9C,QAAO,WAAW,MAAM,QAAQ,OAAO,CAAC;;AAG1C,SAAS,kBAAkB,OAAe;AACxC,QAAO,MAAM,UAAU,CAAC,MAAM,IAAI,CAAC,IAAI,UAAU;;AAGnD,SAAgB,qBAAqB,QAAgB,MAAc;CACjE,MAAM,UAAUA,cAAAA,MAAM,QAAQ,GAAG,IAAI;CACrC,MAAM,OAAO,KAAK,KAAK,UAAU,KAAK;CACtC,MAAM,MAAM,KAAK,MAAM,UAAU,KAAK;AACtC,QAAO,QACL,QAAQ,UAAU,OAAQ,OAAO,SAAS,MAAM,IAAI,OAAO,OAAQ,MAAM,MACzE,kBAAkB,KAAK,CACxB;;AAyBH,SAAgB,cACd,UACA,EAAE,OAAO,KAAM,aAAa,cAAc,eAAqC,EAAE,EACpD;CAC7B,MAAM,CAAC,QAAQ,cAAA,GAAA,MAAA,UAAsB,MAAM;AAgF3C,QAAO;EAAE,MAAA,GAAA,MAAA,cA7EN,SAAS;GACR,MAAM,UAAU,OAAmB,OAAO,UAAU;AAClD,QAAI,MAAM;AACR,UAAK,MAAM,aAAa;KAExB,MAAM,WAAW,qBADL,SAAS,CAAC,MAAM,SAAS,MAAM,QAAQ,EAAE,KAAK,EACf,QAAQ,EAAE;AAErD,cAAS,SAAS;AAClB,aAAQ,cAAc,SAAS;;;GAInC,MAAM,sBAAsB;AAC1B,oBAAgB;AAChB,cAAU,KAAK;AACf,aAAS,iBAAiB,aAAa,iBAAiB,MAAM;AAC9D,aAAS,iBAAiB,WAAW,eAAe,MAAM;AAC1D,aAAS,iBAAiB,aAAa,iBAAiB,EAAE,SAAS,OAAO,CAAC;AAC3E,aAAS,iBAAiB,YAAY,gBAAgB,MAAM;;GAG9D,MAAM,oBAAoB;AACxB,kBAAc;AACd,cAAU,MAAM;AAChB,aAAS,oBAAoB,aAAa,iBAAiB,MAAM;AACjE,aAAS,oBAAoB,WAAW,eAAe,MAAM;AAC7D,aAAS,oBAAoB,aAAa,iBAAiB,MAAM;AACjE,aAAS,oBAAoB,YAAY,gBAAgB,MAAM;;GAGjE,MAAM,eAAe,UAAsB;AACzC,mBAAe;AACf,WAAO,MAAM;;GAGf,MAAM,mBAAmB,UAAsB;AAC7C,WAAO,MAAM;;GAGf,MAAM,iBAAiB,UAAsB;AAC3C,WAAO,OAAO,KAAK;AACnB,iBAAa;;GAGf,MAAM,mBAAmB,UAAsB;AAC7C,UAAM,gBAAgB;AACtB,WAAO,MAAM,QAAQ,GAAU;;GAGjC,MAAM,kBAAkB,UAAsB;AAC5C,WAAO,MAAM,eAAe,IAAW,KAAK;AAC5C,iBAAa;;GAGf,MAAM,oBAAoB,UAAsB;AAC9C,UAAM,gBAAgB;AACtB,mBAAe;AACf,WAAO,MAAM,QAAQ,GAAU;;AAGjC,SAAM,iBAAiB,aAAa,YAAY;AAChD,SAAM,iBAAiB,cAAc,kBAAkB,EAAE,SAAS,OAAO,CAAC;AAE1E,gBAAa;AACX,QAAI,MAAM;AACR,UAAK,oBAAoB,aAAa,YAAY;AAClD,UAAK,oBAAoB,cAAc,iBAAiB;;AAE1D,aAAS,oBAAoB,aAAa,iBAAiB,MAAM;AACjE,aAAS,oBAAoB,WAAW,eAAe,MAAM;AAC7D,aAAS,oBAAoB,aAAa,iBAAiB,MAAM;AACjE,aAAS,oBAAoB,YAAY,gBAAgB,MAAM;;KAGnE,CAAC,SAAS,CACX;EAE0B;EAAQ"}
1
+ {"version":3,"file":"use-radial-move.cjs","names":["clamp"],"sources":["../../src/use-radial-move/use-radial-move.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\nimport { clamp } from '../utils';\n\nfunction radiansToDegrees(radians: number) {\n return radians * (180 / Math.PI);\n}\n\nfunction getElementCenter(element: HTMLElement) {\n const rect = element.getBoundingClientRect();\n return [rect.left + rect.width / 2, rect.top + rect.height / 2];\n}\n\nfunction getAngle(coordinates: [number, number], element: HTMLElement) {\n const center = getElementCenter(element);\n const x = coordinates[0] - center[0];\n const y = coordinates[1] - center[1];\n const deg = radiansToDegrees(Math.atan2(x, y)) + 180;\n return 360 - deg;\n}\n\nfunction toFixed(value: number, digits: number) {\n return parseFloat(value.toFixed(digits));\n}\n\nfunction getDigitsAfterDot(value: number) {\n return value.toString().split('.')[1]?.length || 0;\n}\n\nexport function normalizeRadialValue(degree: number, step: number) {\n const clamped = clamp(degree, 0, 360);\n const high = Math.ceil(clamped / step);\n const low = Math.round(clamped / step);\n return toFixed(\n high >= clamped / step ? (high * step === 360 ? 0 : high * step) : low * step,\n getDigitsAfterDot(step)\n );\n}\n\nexport interface UseRadialMoveOptions {\n /** Number by which value is incremented/decremented with mouse and touch events, `0.01` by default */\n step?: number;\n\n /** Called in `onMouseUp` and `onTouchEnd` events with the current value */\n onChangeEnd?: (value: number) => void;\n\n /** Called in `onMouseDown` and `onTouchStart` events */\n onScrubStart?: () => void;\n\n /** Called in `onMouseUp` and `onTouchEnd` events */\n onScrubEnd?: () => void;\n}\n\nexport interface UseRadialMoveReturnValue<T extends HTMLElement = any> {\n /** Ref to be passed to the element that should be used for radial move */\n ref: React.RefCallback<T | null>;\n\n /** Indicates whether the radial move is active */\n active: boolean;\n}\n\nexport function useRadialMove<T extends HTMLElement = any>(\n onChange: (value: number) => void,\n { step = 0.01, onChangeEnd, onScrubStart, onScrubEnd }: UseRadialMoveOptions = {}\n): UseRadialMoveReturnValue<T> {\n const [active, setActive] = useState(false);\n const cleanupRef = useRef<(() => void) | null>(null);\n\n useEffect(() => {\n return () => {\n cleanupRef.current?.();\n };\n }, []);\n\n const refCallback: React.RefCallback<T | null> = useCallback(\n (node) => {\n const update = (event: MouseEvent, done = false) => {\n if (node) {\n node.style.userSelect = 'none';\n const deg = getAngle([event.clientX, event.clientY], node);\n const newValue = normalizeRadialValue(deg, step || 1);\n\n onChange(newValue);\n done && onChangeEnd?.(newValue);\n }\n };\n\n const beginTracking = () => {\n onScrubStart?.();\n setActive(true);\n document.addEventListener('mousemove', handleMouseMove, false);\n document.addEventListener('mouseup', handleMouseUp, false);\n document.addEventListener('touchmove', handleTouchMove, { passive: false });\n document.addEventListener('touchend', handleTouchEnd, false);\n };\n\n const endTracking = () => {\n onScrubEnd?.();\n setActive(false);\n document.removeEventListener('mousemove', handleMouseMove, false);\n document.removeEventListener('mouseup', handleMouseUp, false);\n document.removeEventListener('touchmove', handleTouchMove, false);\n document.removeEventListener('touchend', handleTouchEnd, false);\n };\n\n const onMouseDown = (event: MouseEvent) => {\n beginTracking();\n update(event);\n };\n\n const handleMouseMove = (event: MouseEvent) => {\n update(event);\n };\n\n const handleMouseUp = (event: MouseEvent) => {\n update(event, true);\n endTracking();\n };\n\n const handleTouchMove = (event: TouchEvent) => {\n event.preventDefault();\n update(event.touches[0] as any);\n };\n\n const handleTouchEnd = (event: TouchEvent) => {\n update(event.changedTouches[0] as any, true);\n endTracking();\n };\n\n const handleTouchStart = (event: TouchEvent) => {\n event.preventDefault();\n beginTracking();\n update(event.touches[0] as any);\n };\n\n node?.addEventListener('mousedown', onMouseDown);\n node?.addEventListener('touchstart', handleTouchStart, { passive: false });\n\n cleanupRef.current = () => {\n document.removeEventListener('mousemove', handleMouseMove, false);\n document.removeEventListener('mouseup', handleMouseUp, false);\n document.removeEventListener('touchmove', handleTouchMove, false);\n document.removeEventListener('touchend', handleTouchEnd, false);\n };\n\n return () => {\n if (node) {\n node.removeEventListener('mousedown', onMouseDown);\n node.removeEventListener('touchstart', handleTouchStart);\n }\n };\n },\n [onChange]\n );\n\n return { ref: refCallback, active };\n}\n\nexport namespace useRadialMove {\n export type Options = UseRadialMoveOptions;\n export type ReturnValue<T extends HTMLElement> = UseRadialMoveReturnValue<T>;\n}\n"],"mappings":"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"}
package/esm/index.mjs CHANGED
@@ -86,4 +86,5 @@ import { useCollapse } from "./use-collapse/use-collapse.mjs";
86
86
  import { useHorizontalCollapse } from "./use-collapse/use-horizontal-collapse.mjs";
87
87
  import { formatMask, generatePattern, isMaskComplete, unformatMask, useMask } from "./use-mask/use-mask.mjs";
88
88
  import { useRovingIndex } from "./use-roving-index/use-roving-index.mjs";
89
- export { assignRef, clamp, clampUseMovePosition, formatMask, generatePattern, getHotkeyHandler, isMaskComplete, lowerFirst, mergeRefs, normalizeRadialValue, randomId, range, readLocalStorageValue, readSessionStorageValue, shallowEqual, unformatMask, upperFirst, useCallbackRef, useClickOutside, useClipboard, useCollapse, useColorScheme, useCounter, useDebouncedCallback, useDebouncedState, useDebouncedValue, useDidUpdate, useDisclosure, useDocumentTitle, useDocumentVisibility, useElementSize, useEventListener, useEyeDropper, useFavicon, useFetch, useFileDialog, useFloatingWindow, useFocusReturn, useFocusTrap, useFocusWithin, useForceUpdate, useFullscreenDocument, useFullscreenElement, useHash, useHeadroom, useHorizontalCollapse, useHotkeys, useHover, useId, useIdle, useInViewport, useInputState, useIntersection, useInterval, useIsFirstRender, useIsomorphicEffect, useListState, useLocalStorage, useLogger, useLongPress, useMap, useMask, useMediaQuery, useMergedRef, useMounted, useMouse, useMousePosition, useMove, useMutationObserver, useMutationObserverTarget, useNetwork, useOrientation, useOs, usePageLeave, usePagination, usePrevious, useQueue, useRadialMove, useReducedMotion, useResizeObserver, useRovingIndex, useScrollDirection, useScrollIntoView, useScrollSpy, useScroller, useSelection, useSessionStorage, useSet, useSetState, useShallowEffect, useStateHistory, useTextSelection, useThrottledCallback, useThrottledState, useThrottledValue, useTimeout, useToggle, useUncontrolled, useValidatedState, useViewportSize, useWindowEvent, useWindowScroll };
89
+ import { useDrag } from "./use-drag/use-drag.mjs";
90
+ export { assignRef, clamp, clampUseMovePosition, formatMask, generatePattern, getHotkeyHandler, isMaskComplete, lowerFirst, mergeRefs, normalizeRadialValue, randomId, range, readLocalStorageValue, readSessionStorageValue, shallowEqual, unformatMask, upperFirst, useCallbackRef, useClickOutside, useClipboard, useCollapse, useColorScheme, useCounter, useDebouncedCallback, useDebouncedState, useDebouncedValue, useDidUpdate, useDisclosure, useDocumentTitle, useDocumentVisibility, useDrag, useElementSize, useEventListener, useEyeDropper, useFavicon, useFetch, useFileDialog, useFloatingWindow, useFocusReturn, useFocusTrap, useFocusWithin, useForceUpdate, useFullscreenDocument, useFullscreenElement, useHash, useHeadroom, useHorizontalCollapse, useHotkeys, useHover, useId, useIdle, useInViewport, useInputState, useIntersection, useInterval, useIsFirstRender, useIsomorphicEffect, useListState, useLocalStorage, useLogger, useLongPress, useMap, useMask, useMediaQuery, useMergedRef, useMounted, useMouse, useMousePosition, useMove, useMutationObserver, useMutationObserverTarget, useNetwork, useOrientation, useOs, usePageLeave, usePagination, usePrevious, useQueue, useRadialMove, useReducedMotion, useResizeObserver, useRovingIndex, useScrollDirection, useScrollIntoView, useScrollSpy, useScroller, useSelection, useSessionStorage, useSet, useSetState, useShallowEffect, useStateHistory, useTextSelection, useThrottledCallback, useThrottledState, useThrottledValue, useTimeout, useToggle, useUncontrolled, useValidatedState, useViewportSize, useWindowEvent, useWindowScroll };
@@ -0,0 +1,266 @@
1
+ "use client";
2
+ import { useCallback, useRef, useState } from "react";
3
+ //#region packages/@mantine/hooks/src/use-drag/use-drag.ts
4
+ const VELOCITY_DECAY_MS = 100;
5
+ function sign(n) {
6
+ if (n > 0) return 1;
7
+ if (n < 0) return -1;
8
+ return 0;
9
+ }
10
+ function getThresholdVector(threshold) {
11
+ const t = threshold ?? 0;
12
+ if (typeof t === "number") return [t, t];
13
+ return t;
14
+ }
15
+ function createInitialState() {
16
+ return {
17
+ isActive: false,
18
+ pointerId: -1,
19
+ startXY: [0, 0],
20
+ prevXY: [0, 0],
21
+ startTimestamp: 0,
22
+ prevTimestamp: 0,
23
+ thresholdMet: false,
24
+ firstFired: false,
25
+ lockedAxis: null,
26
+ canceled: false,
27
+ lastVelocity: [0, 0]
28
+ };
29
+ }
30
+ function useDrag(handler, options = {}) {
31
+ const [active, setActive] = useState(false);
32
+ const handlerRef = useRef(handler);
33
+ handlerRef.current = handler;
34
+ const optionsRef = useRef(options);
35
+ optionsRef.current = options;
36
+ const stateRef = useRef(createInitialState());
37
+ const documentControllerRef = useRef(null);
38
+ return {
39
+ ref: useCallback((node) => {
40
+ if (!node) return;
41
+ const elementController = new AbortController();
42
+ const applyAxisConstraint = (v) => {
43
+ const opts = optionsRef.current;
44
+ const s = stateRef.current;
45
+ if (opts.axis === "x") return [v[0], 0];
46
+ if (opts.axis === "y") return [0, v[1]];
47
+ if (opts.axis === "lock") {
48
+ if (s.lockedAxis === null) {
49
+ const t = opts.axisThreshold ?? 1;
50
+ if (Math.abs(v[0]) > t || Math.abs(v[1]) > t) s.lockedAxis = Math.abs(v[0]) >= Math.abs(v[1]) ? "x" : "y";
51
+ }
52
+ if (s.lockedAxis === "x") return [v[0], 0];
53
+ if (s.lockedAxis === "y") return [0, v[1]];
54
+ }
55
+ return v;
56
+ };
57
+ const resetDrag = () => {
58
+ const s = stateRef.current;
59
+ s.isActive = false;
60
+ s.pointerId = -1;
61
+ s.thresholdMet = false;
62
+ s.firstFired = false;
63
+ s.lockedAxis = null;
64
+ s.canceled = false;
65
+ setActive(false);
66
+ document.body.style.userSelect = "";
67
+ document.body.style.webkitUserSelect = "";
68
+ documentControllerRef.current?.abort();
69
+ documentControllerRef.current = null;
70
+ };
71
+ const cancel = () => {
72
+ if (stateRef.current.isActive) {
73
+ stateRef.current.canceled = true;
74
+ resetDrag();
75
+ }
76
+ };
77
+ const activateDrag = () => {
78
+ setActive(true);
79
+ document.body.style.userSelect = "none";
80
+ document.body.style.webkitUserSelect = "none";
81
+ };
82
+ const onPointerDown = (event) => {
83
+ if (optionsRef.current.enabled === false) return;
84
+ if (event.button !== 0) return;
85
+ if (stateRef.current.isActive) return;
86
+ const s = stateRef.current;
87
+ s.isActive = true;
88
+ s.pointerId = event.pointerId;
89
+ s.startXY = [event.clientX, event.clientY];
90
+ s.prevXY = [event.clientX, event.clientY];
91
+ s.startTimestamp = event.timeStamp;
92
+ s.prevTimestamp = event.timeStamp;
93
+ s.thresholdMet = false;
94
+ s.firstFired = false;
95
+ s.lockedAxis = null;
96
+ s.canceled = false;
97
+ s.lastVelocity = [0, 0];
98
+ const [tx, ty] = getThresholdVector(optionsRef.current.threshold);
99
+ if (tx === 0 && ty === 0) {
100
+ s.thresholdMet = true;
101
+ s.firstFired = true;
102
+ activateDrag();
103
+ handlerRef.current({
104
+ xy: [event.clientX, event.clientY],
105
+ initial: [event.clientX, event.clientY],
106
+ movement: [0, 0],
107
+ delta: [0, 0],
108
+ distance: [0, 0],
109
+ direction: [0, 0],
110
+ velocity: [0, 0],
111
+ elapsedTime: 0,
112
+ first: true,
113
+ last: false,
114
+ active: true,
115
+ tap: false,
116
+ canceled: false,
117
+ cancel,
118
+ event
119
+ });
120
+ }
121
+ documentControllerRef.current?.abort();
122
+ documentControllerRef.current = new AbortController();
123
+ const sig = documentControllerRef.current.signal;
124
+ document.addEventListener("pointermove", onPointerMove, { signal: sig });
125
+ document.addEventListener("pointerup", onPointerUp, { signal: sig });
126
+ document.addEventListener("pointercancel", onPointerCancel, { signal: sig });
127
+ };
128
+ const onPointerMove = (event) => {
129
+ const s = stateRef.current;
130
+ if (!s.isActive || event.pointerId !== s.pointerId) return;
131
+ const rawMovement = [event.clientX - s.startXY[0], event.clientY - s.startXY[1]];
132
+ if (!s.thresholdMet) {
133
+ const [tx, ty] = getThresholdVector(optionsRef.current.threshold);
134
+ if (Math.abs(rawMovement[0]) < tx && Math.abs(rawMovement[1]) < ty) {
135
+ s.prevXY = [event.clientX, event.clientY];
136
+ s.prevTimestamp = event.timeStamp;
137
+ return;
138
+ }
139
+ s.thresholdMet = true;
140
+ activateDrag();
141
+ }
142
+ const movement = applyAxisConstraint(rawMovement);
143
+ const delta = applyAxisConstraint([event.clientX - s.prevXY[0], event.clientY - s.prevXY[1]]);
144
+ const timeDelta = event.timeStamp - s.prevTimestamp;
145
+ const velocity = timeDelta > 0 ? [Math.abs(delta[0]) / timeDelta, Math.abs(delta[1]) / timeDelta] : s.lastVelocity;
146
+ s.lastVelocity = velocity;
147
+ const isFirst = !s.firstFired;
148
+ s.firstFired = true;
149
+ s.prevXY = [event.clientX, event.clientY];
150
+ s.prevTimestamp = event.timeStamp;
151
+ handlerRef.current({
152
+ xy: [event.clientX, event.clientY],
153
+ initial: [...s.startXY],
154
+ movement,
155
+ delta,
156
+ distance: [Math.abs(movement[0]), Math.abs(movement[1])],
157
+ direction: [sign(delta[0]), sign(delta[1])],
158
+ velocity,
159
+ elapsedTime: event.timeStamp - s.startTimestamp,
160
+ first: isFirst,
161
+ last: false,
162
+ active: true,
163
+ tap: false,
164
+ canceled: false,
165
+ cancel,
166
+ event
167
+ });
168
+ };
169
+ const onPointerUp = (event) => {
170
+ const s = stateRef.current;
171
+ if (!s.isActive || event.pointerId !== s.pointerId) return;
172
+ const opts = optionsRef.current;
173
+ if (!s.thresholdMet) {
174
+ if (opts.filterTaps) {
175
+ const mov = applyAxisConstraint([event.clientX - s.startXY[0], event.clientY - s.startXY[1]]);
176
+ const dist = [Math.abs(mov[0]), Math.abs(mov[1])];
177
+ const isTap = Math.max(dist[0], dist[1]) < (opts.tapThreshold ?? 3);
178
+ handlerRef.current({
179
+ xy: [event.clientX, event.clientY],
180
+ initial: [...s.startXY],
181
+ movement: mov,
182
+ delta: mov,
183
+ distance: dist,
184
+ direction: [sign(mov[0]), sign(mov[1])],
185
+ velocity: [0, 0],
186
+ elapsedTime: event.timeStamp - s.startTimestamp,
187
+ first: true,
188
+ last: true,
189
+ active: false,
190
+ tap: isTap,
191
+ canceled: false,
192
+ cancel,
193
+ event
194
+ });
195
+ }
196
+ resetDrag();
197
+ return;
198
+ }
199
+ const movement = applyAxisConstraint([event.clientX - s.startXY[0], event.clientY - s.startXY[1]]);
200
+ const distance = [Math.abs(movement[0]), Math.abs(movement[1])];
201
+ const delta = applyAxisConstraint([event.clientX - s.prevXY[0], event.clientY - s.prevXY[1]]);
202
+ const velocity = event.timeStamp - s.prevTimestamp > VELOCITY_DECAY_MS ? [0, 0] : s.lastVelocity;
203
+ const maxDistance = Math.max(distance[0], distance[1]);
204
+ const tap = opts.filterTaps === true && maxDistance < (opts.tapThreshold ?? 3);
205
+ handlerRef.current({
206
+ xy: [event.clientX, event.clientY],
207
+ initial: [...s.startXY],
208
+ movement,
209
+ delta,
210
+ distance,
211
+ direction: [sign(delta[0]), sign(delta[1])],
212
+ velocity,
213
+ elapsedTime: event.timeStamp - s.startTimestamp,
214
+ first: !s.firstFired,
215
+ last: true,
216
+ active: false,
217
+ tap,
218
+ canceled: false,
219
+ cancel,
220
+ event
221
+ });
222
+ resetDrag();
223
+ };
224
+ const onPointerCancel = (event) => {
225
+ const s = stateRef.current;
226
+ if (!s.isActive || event.pointerId !== s.pointerId) return;
227
+ const movement = applyAxisConstraint([event.clientX - s.startXY[0], event.clientY - s.startXY[1]]);
228
+ handlerRef.current({
229
+ xy: [event.clientX, event.clientY],
230
+ initial: [...s.startXY],
231
+ movement,
232
+ delta: [0, 0],
233
+ distance: [Math.abs(movement[0]), Math.abs(movement[1])],
234
+ direction: [0, 0],
235
+ velocity: [0, 0],
236
+ elapsedTime: event.timeStamp - s.startTimestamp,
237
+ first: !s.firstFired,
238
+ last: true,
239
+ active: false,
240
+ tap: false,
241
+ canceled: true,
242
+ cancel,
243
+ event
244
+ });
245
+ resetDrag();
246
+ };
247
+ node.addEventListener("pointerdown", onPointerDown, { signal: elementController.signal });
248
+ return () => {
249
+ elementController.abort();
250
+ documentControllerRef.current?.abort();
251
+ documentControllerRef.current = null;
252
+ if (stateRef.current.isActive) {
253
+ stateRef.current.isActive = false;
254
+ setActive(false);
255
+ document.body.style.userSelect = "";
256
+ document.body.style.webkitUserSelect = "";
257
+ }
258
+ };
259
+ }, []),
260
+ active
261
+ };
262
+ }
263
+ //#endregion
264
+ export { useDrag };
265
+
266
+ //# sourceMappingURL=use-drag.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-drag.mjs","names":[],"sources":["../../src/use-drag/use-drag.ts"],"sourcesContent":["// Required to disable for webkit-user-select, although deprecated, it is still required for Safari support\n/* eslint-disable @typescript-eslint/no-deprecated */\nimport { useCallback, useRef, useState } from 'react';\n\ntype Vector2 = [number, number];\n\nexport interface UseDragState {\n /** Current pointer position [x, y] */\n xy: Vector2;\n\n /** Position where the gesture started [x, y] */\n initial: Vector2;\n\n /** Displacement from start [x, y], respects axis constraint */\n movement: Vector2;\n\n /** Change since previous event [x, y] */\n delta: Vector2;\n\n /** Absolute distance per axis [x, y] */\n distance: Vector2;\n\n /** Movement direction per axis: -1, 0, or 1 */\n direction: Vector2;\n\n /** Speed per axis in px/ms */\n velocity: Vector2;\n\n /** Time since drag started in ms */\n elapsedTime: number;\n\n /** `true` on the first handler call after the threshold is met */\n first: boolean;\n\n /** `true` on the last handler call (pointer released or canceled) */\n last: boolean;\n\n /** `true` while the gesture is ongoing */\n active: boolean;\n\n /** `true` when the gesture qualifies as a tap (requires `filterTaps: true`) */\n tap: boolean;\n\n /** `true` when the gesture was interrupted by a `pointercancel` event */\n canceled: boolean;\n\n /** Function to programmatically cancel the current gesture */\n cancel: () => void;\n\n /** The source `PointerEvent` */\n event: PointerEvent;\n}\n\nexport interface UseDragOptions {\n /** Constrain movement to a specific axis. `'lock'` locks to whichever axis has more movement after `axisThreshold` is exceeded. */\n axis?: 'x' | 'y' | 'lock';\n\n /** Movement in px required to determine axis when `axis` is `'lock'`, `1` by default */\n axisThreshold?: number;\n\n /** When `true`, the last state includes `tap: true` when total distance is below `tapThreshold`, `false` by default */\n filterTaps?: boolean;\n\n /** Max displacement in px to still be considered a tap, `3` by default */\n tapThreshold?: number;\n\n /** Minimum displacement in px before the drag activates. Can be a number (both axes) or `[x, y]`. `0` by default */\n threshold?: number | Vector2;\n\n /** Enable or disable the hook, `true` by default */\n enabled?: boolean;\n}\n\nexport interface UseDragReturnValue<T extends HTMLElement = any> {\n /** Ref callback to attach to the draggable element */\n ref: React.RefCallback<T | null>;\n\n /** `true` while the element is being dragged */\n active: boolean;\n}\n\nconst VELOCITY_DECAY_MS = 100;\n\nfunction sign(n: number): -1 | 0 | 1 {\n if (n > 0) {\n return 1;\n }\n if (n < 0) {\n return -1;\n }\n return 0;\n}\n\nfunction getThresholdVector(threshold: number | Vector2 | undefined): Vector2 {\n const t = threshold ?? 0;\n if (typeof t === 'number') {\n return [t, t];\n }\n return t;\n}\n\ninterface DragInternalState {\n isActive: boolean;\n pointerId: number;\n startXY: Vector2;\n prevXY: Vector2;\n startTimestamp: number;\n prevTimestamp: number;\n thresholdMet: boolean;\n firstFired: boolean;\n lockedAxis: 'x' | 'y' | null;\n canceled: boolean;\n lastVelocity: Vector2;\n}\n\nfunction createInitialState(): DragInternalState {\n return {\n isActive: false,\n pointerId: -1,\n startXY: [0, 0],\n prevXY: [0, 0],\n startTimestamp: 0,\n prevTimestamp: 0,\n thresholdMet: false,\n firstFired: false,\n lockedAxis: null,\n canceled: false,\n lastVelocity: [0, 0],\n };\n}\n\nexport function useDrag<T extends HTMLElement = any>(\n handler: (state: UseDragState) => void,\n options: UseDragOptions = {}\n): UseDragReturnValue<T> {\n const [active, setActive] = useState(false);\n\n const handlerRef = useRef(handler);\n handlerRef.current = handler;\n\n const optionsRef = useRef(options);\n optionsRef.current = options;\n\n const stateRef = useRef<DragInternalState>(createInitialState());\n const documentControllerRef = useRef<AbortController | null>(null);\n\n const refCallback: React.RefCallback<T | null> = useCallback((node) => {\n if (!node) {\n return undefined;\n }\n\n const elementController = new AbortController();\n\n const applyAxisConstraint = (v: Vector2): Vector2 => {\n const opts = optionsRef.current;\n const s = stateRef.current;\n\n if (opts.axis === 'x') {\n return [v[0], 0];\n }\n if (opts.axis === 'y') {\n return [0, v[1]];\n }\n if (opts.axis === 'lock') {\n if (s.lockedAxis === null) {\n const t = opts.axisThreshold ?? 1;\n if (Math.abs(v[0]) > t || Math.abs(v[1]) > t) {\n s.lockedAxis = Math.abs(v[0]) >= Math.abs(v[1]) ? 'x' : 'y';\n }\n }\n if (s.lockedAxis === 'x') {\n return [v[0], 0];\n }\n if (s.lockedAxis === 'y') {\n return [0, v[1]];\n }\n }\n return v;\n };\n\n const resetDrag = () => {\n const s = stateRef.current;\n s.isActive = false;\n s.pointerId = -1;\n s.thresholdMet = false;\n s.firstFired = false;\n s.lockedAxis = null;\n s.canceled = false;\n setActive(false);\n document.body.style.userSelect = '';\n document.body.style.webkitUserSelect = '';\n documentControllerRef.current?.abort();\n documentControllerRef.current = null;\n };\n\n const cancel = () => {\n if (stateRef.current.isActive) {\n stateRef.current.canceled = true;\n resetDrag();\n }\n };\n\n const activateDrag = () => {\n setActive(true);\n document.body.style.userSelect = 'none';\n document.body.style.webkitUserSelect = 'none';\n };\n\n const onPointerDown = (event: PointerEvent) => {\n if (optionsRef.current.enabled === false) {\n return;\n }\n if (event.button !== 0) {\n return;\n }\n if (stateRef.current.isActive) {\n return;\n }\n\n const s = stateRef.current;\n s.isActive = true;\n s.pointerId = event.pointerId;\n s.startXY = [event.clientX, event.clientY];\n s.prevXY = [event.clientX, event.clientY];\n s.startTimestamp = event.timeStamp;\n s.prevTimestamp = event.timeStamp;\n s.thresholdMet = false;\n s.firstFired = false;\n s.lockedAxis = null;\n s.canceled = false;\n s.lastVelocity = [0, 0];\n\n const [tx, ty] = getThresholdVector(optionsRef.current.threshold);\n if (tx === 0 && ty === 0) {\n s.thresholdMet = true;\n s.firstFired = true;\n activateDrag();\n\n handlerRef.current({\n xy: [event.clientX, event.clientY],\n initial: [event.clientX, event.clientY],\n movement: [0, 0],\n delta: [0, 0],\n distance: [0, 0],\n direction: [0, 0],\n velocity: [0, 0],\n elapsedTime: 0,\n first: true,\n last: false,\n active: true,\n tap: false,\n canceled: false,\n cancel,\n event,\n });\n }\n\n documentControllerRef.current?.abort();\n documentControllerRef.current = new AbortController();\n const sig = documentControllerRef.current.signal;\n\n document.addEventListener('pointermove', onPointerMove, { signal: sig });\n document.addEventListener('pointerup', onPointerUp, { signal: sig });\n document.addEventListener('pointercancel', onPointerCancel, { signal: sig });\n };\n\n const onPointerMove = (event: PointerEvent) => {\n const s = stateRef.current;\n if (!s.isActive || event.pointerId !== s.pointerId) {\n return;\n }\n\n const rawMovement: Vector2 = [event.clientX - s.startXY[0], event.clientY - s.startXY[1]];\n\n if (!s.thresholdMet) {\n const [tx, ty] = getThresholdVector(optionsRef.current.threshold);\n if (Math.abs(rawMovement[0]) < tx && Math.abs(rawMovement[1]) < ty) {\n s.prevXY = [event.clientX, event.clientY];\n s.prevTimestamp = event.timeStamp;\n return;\n }\n s.thresholdMet = true;\n activateDrag();\n }\n\n const movement = applyAxisConstraint(rawMovement);\n const rawDelta: Vector2 = [event.clientX - s.prevXY[0], event.clientY - s.prevXY[1]];\n const delta = applyAxisConstraint(rawDelta);\n const timeDelta = event.timeStamp - s.prevTimestamp;\n const velocity: Vector2 =\n timeDelta > 0\n ? [Math.abs(delta[0]) / timeDelta, Math.abs(delta[1]) / timeDelta]\n : s.lastVelocity;\n\n s.lastVelocity = velocity;\n const isFirst = !s.firstFired;\n s.firstFired = true;\n s.prevXY = [event.clientX, event.clientY];\n s.prevTimestamp = event.timeStamp;\n\n handlerRef.current({\n xy: [event.clientX, event.clientY],\n initial: [...s.startXY],\n movement,\n delta,\n distance: [Math.abs(movement[0]), Math.abs(movement[1])],\n direction: [sign(delta[0]), sign(delta[1])],\n velocity,\n elapsedTime: event.timeStamp - s.startTimestamp,\n first: isFirst,\n last: false,\n active: true,\n tap: false,\n canceled: false,\n cancel,\n event,\n });\n };\n\n const onPointerUp = (event: PointerEvent) => {\n const s = stateRef.current;\n if (!s.isActive || event.pointerId !== s.pointerId) {\n return;\n }\n\n const opts = optionsRef.current;\n\n if (!s.thresholdMet) {\n if (opts.filterTaps) {\n const rawMov: Vector2 = [event.clientX - s.startXY[0], event.clientY - s.startXY[1]];\n const mov = applyAxisConstraint(rawMov);\n const dist: Vector2 = [Math.abs(mov[0]), Math.abs(mov[1])];\n const maxDist = Math.max(dist[0], dist[1]);\n const isTap = maxDist < (opts.tapThreshold ?? 3);\n\n handlerRef.current({\n xy: [event.clientX, event.clientY],\n initial: [...s.startXY],\n movement: mov,\n delta: mov,\n distance: dist,\n direction: [sign(mov[0]), sign(mov[1])],\n velocity: [0, 0],\n elapsedTime: event.timeStamp - s.startTimestamp,\n first: true,\n last: true,\n active: false,\n tap: isTap,\n canceled: false,\n cancel,\n event,\n });\n }\n resetDrag();\n return;\n }\n\n const rawMovement: Vector2 = [event.clientX - s.startXY[0], event.clientY - s.startXY[1]];\n const movement = applyAxisConstraint(rawMovement);\n const distance: Vector2 = [Math.abs(movement[0]), Math.abs(movement[1])];\n const rawDelta: Vector2 = [event.clientX - s.prevXY[0], event.clientY - s.prevXY[1]];\n const delta = applyAxisConstraint(rawDelta);\n\n const timeSinceLastMove = event.timeStamp - s.prevTimestamp;\n const velocity: Vector2 = timeSinceLastMove > VELOCITY_DECAY_MS ? [0, 0] : s.lastVelocity;\n\n const maxDistance = Math.max(distance[0], distance[1]);\n const tap = opts.filterTaps === true && maxDistance < (opts.tapThreshold ?? 3);\n\n handlerRef.current({\n xy: [event.clientX, event.clientY],\n initial: [...s.startXY],\n movement,\n delta,\n distance,\n direction: [sign(delta[0]), sign(delta[1])],\n velocity,\n elapsedTime: event.timeStamp - s.startTimestamp,\n first: !s.firstFired,\n last: true,\n active: false,\n tap,\n canceled: false,\n cancel,\n event,\n });\n\n resetDrag();\n };\n\n const onPointerCancel = (event: PointerEvent) => {\n const s = stateRef.current;\n if (!s.isActive || event.pointerId !== s.pointerId) {\n return;\n }\n\n const rawMovement: Vector2 = [event.clientX - s.startXY[0], event.clientY - s.startXY[1]];\n const movement = applyAxisConstraint(rawMovement);\n\n handlerRef.current({\n xy: [event.clientX, event.clientY],\n initial: [...s.startXY],\n movement,\n delta: [0, 0],\n distance: [Math.abs(movement[0]), Math.abs(movement[1])],\n direction: [0, 0],\n velocity: [0, 0],\n elapsedTime: event.timeStamp - s.startTimestamp,\n first: !s.firstFired,\n last: true,\n active: false,\n tap: false,\n canceled: true,\n cancel,\n event,\n });\n\n resetDrag();\n };\n\n node.addEventListener('pointerdown', onPointerDown, {\n signal: elementController.signal,\n });\n\n return () => {\n elementController.abort();\n documentControllerRef.current?.abort();\n documentControllerRef.current = null;\n if (stateRef.current.isActive) {\n stateRef.current.isActive = false;\n setActive(false);\n document.body.style.userSelect = '';\n document.body.style.webkitUserSelect = '';\n }\n };\n }, []);\n\n return { ref: refCallback, active };\n}\n\nexport namespace useDrag {\n export type State = UseDragState;\n export type Options = UseDragOptions;\n export type ReturnValue<T extends HTMLElement = any> = UseDragReturnValue<T>;\n}\n"],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { clamp } from "../utils/clamp/clamp.mjs";
3
- import { useCallback, useState } from "react";
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
4
  //#region packages/@mantine/hooks/src/use-radial-move/use-radial-move.ts
5
5
  function radiansToDegrees(radians) {
6
6
  return radians * (180 / Math.PI);
@@ -29,6 +29,12 @@ function normalizeRadialValue(degree, step) {
29
29
  }
30
30
  function useRadialMove(onChange, { step = .01, onChangeEnd, onScrubStart, onScrubEnd } = {}) {
31
31
  const [active, setActive] = useState(false);
32
+ const cleanupRef = useRef(null);
33
+ useEffect(() => {
34
+ return () => {
35
+ cleanupRef.current?.();
36
+ };
37
+ }, []);
32
38
  return {
33
39
  ref: useCallback((node) => {
34
40
  const update = (event, done = false) => {
@@ -81,15 +87,17 @@ function useRadialMove(onChange, { step = .01, onChangeEnd, onScrubStart, onScru
81
87
  };
82
88
  node?.addEventListener("mousedown", onMouseDown);
83
89
  node?.addEventListener("touchstart", handleTouchStart, { passive: false });
90
+ cleanupRef.current = () => {
91
+ document.removeEventListener("mousemove", handleMouseMove, false);
92
+ document.removeEventListener("mouseup", handleMouseUp, false);
93
+ document.removeEventListener("touchmove", handleTouchMove, false);
94
+ document.removeEventListener("touchend", handleTouchEnd, false);
95
+ };
84
96
  return () => {
85
97
  if (node) {
86
98
  node.removeEventListener("mousedown", onMouseDown);
87
99
  node.removeEventListener("touchstart", handleTouchStart);
88
100
  }
89
- document.removeEventListener("mousemove", handleMouseMove, false);
90
- document.removeEventListener("mouseup", handleMouseUp, false);
91
- document.removeEventListener("touchmove", handleTouchMove, false);
92
- document.removeEventListener("touchend", handleTouchEnd, false);
93
101
  };
94
102
  }, [onChange]),
95
103
  active
@@ -1 +1 @@
1
- {"version":3,"file":"use-radial-move.mjs","names":[],"sources":["../../src/use-radial-move/use-radial-move.ts"],"sourcesContent":["import { useCallback, useState } from 'react';\nimport { clamp } from '../utils';\n\nfunction radiansToDegrees(radians: number) {\n return radians * (180 / Math.PI);\n}\n\nfunction getElementCenter(element: HTMLElement) {\n const rect = element.getBoundingClientRect();\n return [rect.left + rect.width / 2, rect.top + rect.height / 2];\n}\n\nfunction getAngle(coordinates: [number, number], element: HTMLElement) {\n const center = getElementCenter(element);\n const x = coordinates[0] - center[0];\n const y = coordinates[1] - center[1];\n const deg = radiansToDegrees(Math.atan2(x, y)) + 180;\n return 360 - deg;\n}\n\nfunction toFixed(value: number, digits: number) {\n return parseFloat(value.toFixed(digits));\n}\n\nfunction getDigitsAfterDot(value: number) {\n return value.toString().split('.')[1]?.length || 0;\n}\n\nexport function normalizeRadialValue(degree: number, step: number) {\n const clamped = clamp(degree, 0, 360);\n const high = Math.ceil(clamped / step);\n const low = Math.round(clamped / step);\n return toFixed(\n high >= clamped / step ? (high * step === 360 ? 0 : high * step) : low * step,\n getDigitsAfterDot(step)\n );\n}\n\nexport interface UseRadialMoveOptions {\n /** Number by which value is incremented/decremented with mouse and touch events, `0.01` by default */\n step?: number;\n\n /** Called in `onMouseUp` and `onTouchEnd` events with the current value */\n onChangeEnd?: (value: number) => void;\n\n /** Called in `onMouseDown` and `onTouchStart` events */\n onScrubStart?: () => void;\n\n /** Called in `onMouseUp` and `onTouchEnd` events */\n onScrubEnd?: () => void;\n}\n\nexport interface UseRadialMoveReturnValue<T extends HTMLElement = any> {\n /** Ref to be passed to the element that should be used for radial move */\n ref: React.RefCallback<T | null>;\n\n /** Indicates whether the radial move is active */\n active: boolean;\n}\n\nexport function useRadialMove<T extends HTMLElement = any>(\n onChange: (value: number) => void,\n { step = 0.01, onChangeEnd, onScrubStart, onScrubEnd }: UseRadialMoveOptions = {}\n): UseRadialMoveReturnValue<T> {\n const [active, setActive] = useState(false);\n\n const refCallback: React.RefCallback<T | null> = useCallback(\n (node) => {\n const update = (event: MouseEvent, done = false) => {\n if (node) {\n node.style.userSelect = 'none';\n const deg = getAngle([event.clientX, event.clientY], node);\n const newValue = normalizeRadialValue(deg, step || 1);\n\n onChange(newValue);\n done && onChangeEnd?.(newValue);\n }\n };\n\n const beginTracking = () => {\n onScrubStart?.();\n setActive(true);\n document.addEventListener('mousemove', handleMouseMove, false);\n document.addEventListener('mouseup', handleMouseUp, false);\n document.addEventListener('touchmove', handleTouchMove, { passive: false });\n document.addEventListener('touchend', handleTouchEnd, false);\n };\n\n const endTracking = () => {\n onScrubEnd?.();\n setActive(false);\n document.removeEventListener('mousemove', handleMouseMove, false);\n document.removeEventListener('mouseup', handleMouseUp, false);\n document.removeEventListener('touchmove', handleTouchMove, false);\n document.removeEventListener('touchend', handleTouchEnd, false);\n };\n\n const onMouseDown = (event: MouseEvent) => {\n beginTracking();\n update(event);\n };\n\n const handleMouseMove = (event: MouseEvent) => {\n update(event);\n };\n\n const handleMouseUp = (event: MouseEvent) => {\n update(event, true);\n endTracking();\n };\n\n const handleTouchMove = (event: TouchEvent) => {\n event.preventDefault();\n update(event.touches[0] as any);\n };\n\n const handleTouchEnd = (event: TouchEvent) => {\n update(event.changedTouches[0] as any, true);\n endTracking();\n };\n\n const handleTouchStart = (event: TouchEvent) => {\n event.preventDefault();\n beginTracking();\n update(event.touches[0] as any);\n };\n\n node?.addEventListener('mousedown', onMouseDown);\n node?.addEventListener('touchstart', handleTouchStart, { passive: false });\n\n return () => {\n if (node) {\n node.removeEventListener('mousedown', onMouseDown);\n node.removeEventListener('touchstart', handleTouchStart);\n }\n document.removeEventListener('mousemove', handleMouseMove, false);\n document.removeEventListener('mouseup', handleMouseUp, false);\n document.removeEventListener('touchmove', handleTouchMove, false);\n document.removeEventListener('touchend', handleTouchEnd, false);\n };\n },\n [onChange]\n );\n\n return { ref: refCallback, active };\n}\n\nexport namespace useRadialMove {\n export type Options = UseRadialMoveOptions;\n export type ReturnValue<T extends HTMLElement> = UseRadialMoveReturnValue<T>;\n}\n"],"mappings":";;;;AAGA,SAAS,iBAAiB,SAAiB;AACzC,QAAO,WAAW,MAAM,KAAK;;AAG/B,SAAS,iBAAiB,SAAsB;CAC9C,MAAM,OAAO,QAAQ,uBAAuB;AAC5C,QAAO,CAAC,KAAK,OAAO,KAAK,QAAQ,GAAG,KAAK,MAAM,KAAK,SAAS,EAAE;;AAGjE,SAAS,SAAS,aAA+B,SAAsB;CACrE,MAAM,SAAS,iBAAiB,QAAQ;CACxC,MAAM,IAAI,YAAY,KAAK,OAAO;CAClC,MAAM,IAAI,YAAY,KAAK,OAAO;AAElC,QAAO,OADK,iBAAiB,KAAK,MAAM,GAAG,EAAE,CAAC,GAAG;;AAInD,SAAS,QAAQ,OAAe,QAAgB;AAC9C,QAAO,WAAW,MAAM,QAAQ,OAAO,CAAC;;AAG1C,SAAS,kBAAkB,OAAe;AACxC,QAAO,MAAM,UAAU,CAAC,MAAM,IAAI,CAAC,IAAI,UAAU;;AAGnD,SAAgB,qBAAqB,QAAgB,MAAc;CACjE,MAAM,UAAU,MAAM,QAAQ,GAAG,IAAI;CACrC,MAAM,OAAO,KAAK,KAAK,UAAU,KAAK;CACtC,MAAM,MAAM,KAAK,MAAM,UAAU,KAAK;AACtC,QAAO,QACL,QAAQ,UAAU,OAAQ,OAAO,SAAS,MAAM,IAAI,OAAO,OAAQ,MAAM,MACzE,kBAAkB,KAAK,CACxB;;AAyBH,SAAgB,cACd,UACA,EAAE,OAAO,KAAM,aAAa,cAAc,eAAqC,EAAE,EACpD;CAC7B,MAAM,CAAC,QAAQ,aAAa,SAAS,MAAM;AAgF3C,QAAO;EAAE,KA9EwC,aAC9C,SAAS;GACR,MAAM,UAAU,OAAmB,OAAO,UAAU;AAClD,QAAI,MAAM;AACR,UAAK,MAAM,aAAa;KAExB,MAAM,WAAW,qBADL,SAAS,CAAC,MAAM,SAAS,MAAM,QAAQ,EAAE,KAAK,EACf,QAAQ,EAAE;AAErD,cAAS,SAAS;AAClB,aAAQ,cAAc,SAAS;;;GAInC,MAAM,sBAAsB;AAC1B,oBAAgB;AAChB,cAAU,KAAK;AACf,aAAS,iBAAiB,aAAa,iBAAiB,MAAM;AAC9D,aAAS,iBAAiB,WAAW,eAAe,MAAM;AAC1D,aAAS,iBAAiB,aAAa,iBAAiB,EAAE,SAAS,OAAO,CAAC;AAC3E,aAAS,iBAAiB,YAAY,gBAAgB,MAAM;;GAG9D,MAAM,oBAAoB;AACxB,kBAAc;AACd,cAAU,MAAM;AAChB,aAAS,oBAAoB,aAAa,iBAAiB,MAAM;AACjE,aAAS,oBAAoB,WAAW,eAAe,MAAM;AAC7D,aAAS,oBAAoB,aAAa,iBAAiB,MAAM;AACjE,aAAS,oBAAoB,YAAY,gBAAgB,MAAM;;GAGjE,MAAM,eAAe,UAAsB;AACzC,mBAAe;AACf,WAAO,MAAM;;GAGf,MAAM,mBAAmB,UAAsB;AAC7C,WAAO,MAAM;;GAGf,MAAM,iBAAiB,UAAsB;AAC3C,WAAO,OAAO,KAAK;AACnB,iBAAa;;GAGf,MAAM,mBAAmB,UAAsB;AAC7C,UAAM,gBAAgB;AACtB,WAAO,MAAM,QAAQ,GAAU;;GAGjC,MAAM,kBAAkB,UAAsB;AAC5C,WAAO,MAAM,eAAe,IAAW,KAAK;AAC5C,iBAAa;;GAGf,MAAM,oBAAoB,UAAsB;AAC9C,UAAM,gBAAgB;AACtB,mBAAe;AACf,WAAO,MAAM,QAAQ,GAAU;;AAGjC,SAAM,iBAAiB,aAAa,YAAY;AAChD,SAAM,iBAAiB,cAAc,kBAAkB,EAAE,SAAS,OAAO,CAAC;AAE1E,gBAAa;AACX,QAAI,MAAM;AACR,UAAK,oBAAoB,aAAa,YAAY;AAClD,UAAK,oBAAoB,cAAc,iBAAiB;;AAE1D,aAAS,oBAAoB,aAAa,iBAAiB,MAAM;AACjE,aAAS,oBAAoB,WAAW,eAAe,MAAM;AAC7D,aAAS,oBAAoB,aAAa,iBAAiB,MAAM;AACjE,aAAS,oBAAoB,YAAY,gBAAgB,MAAM;;KAGnE,CAAC,SAAS,CACX;EAE0B;EAAQ"}
1
+ {"version":3,"file":"use-radial-move.mjs","names":[],"sources":["../../src/use-radial-move/use-radial-move.ts"],"sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react';\nimport { clamp } from '../utils';\n\nfunction radiansToDegrees(radians: number) {\n return radians * (180 / Math.PI);\n}\n\nfunction getElementCenter(element: HTMLElement) {\n const rect = element.getBoundingClientRect();\n return [rect.left + rect.width / 2, rect.top + rect.height / 2];\n}\n\nfunction getAngle(coordinates: [number, number], element: HTMLElement) {\n const center = getElementCenter(element);\n const x = coordinates[0] - center[0];\n const y = coordinates[1] - center[1];\n const deg = radiansToDegrees(Math.atan2(x, y)) + 180;\n return 360 - deg;\n}\n\nfunction toFixed(value: number, digits: number) {\n return parseFloat(value.toFixed(digits));\n}\n\nfunction getDigitsAfterDot(value: number) {\n return value.toString().split('.')[1]?.length || 0;\n}\n\nexport function normalizeRadialValue(degree: number, step: number) {\n const clamped = clamp(degree, 0, 360);\n const high = Math.ceil(clamped / step);\n const low = Math.round(clamped / step);\n return toFixed(\n high >= clamped / step ? (high * step === 360 ? 0 : high * step) : low * step,\n getDigitsAfterDot(step)\n );\n}\n\nexport interface UseRadialMoveOptions {\n /** Number by which value is incremented/decremented with mouse and touch events, `0.01` by default */\n step?: number;\n\n /** Called in `onMouseUp` and `onTouchEnd` events with the current value */\n onChangeEnd?: (value: number) => void;\n\n /** Called in `onMouseDown` and `onTouchStart` events */\n onScrubStart?: () => void;\n\n /** Called in `onMouseUp` and `onTouchEnd` events */\n onScrubEnd?: () => void;\n}\n\nexport interface UseRadialMoveReturnValue<T extends HTMLElement = any> {\n /** Ref to be passed to the element that should be used for radial move */\n ref: React.RefCallback<T | null>;\n\n /** Indicates whether the radial move is active */\n active: boolean;\n}\n\nexport function useRadialMove<T extends HTMLElement = any>(\n onChange: (value: number) => void,\n { step = 0.01, onChangeEnd, onScrubStart, onScrubEnd }: UseRadialMoveOptions = {}\n): UseRadialMoveReturnValue<T> {\n const [active, setActive] = useState(false);\n const cleanupRef = useRef<(() => void) | null>(null);\n\n useEffect(() => {\n return () => {\n cleanupRef.current?.();\n };\n }, []);\n\n const refCallback: React.RefCallback<T | null> = useCallback(\n (node) => {\n const update = (event: MouseEvent, done = false) => {\n if (node) {\n node.style.userSelect = 'none';\n const deg = getAngle([event.clientX, event.clientY], node);\n const newValue = normalizeRadialValue(deg, step || 1);\n\n onChange(newValue);\n done && onChangeEnd?.(newValue);\n }\n };\n\n const beginTracking = () => {\n onScrubStart?.();\n setActive(true);\n document.addEventListener('mousemove', handleMouseMove, false);\n document.addEventListener('mouseup', handleMouseUp, false);\n document.addEventListener('touchmove', handleTouchMove, { passive: false });\n document.addEventListener('touchend', handleTouchEnd, false);\n };\n\n const endTracking = () => {\n onScrubEnd?.();\n setActive(false);\n document.removeEventListener('mousemove', handleMouseMove, false);\n document.removeEventListener('mouseup', handleMouseUp, false);\n document.removeEventListener('touchmove', handleTouchMove, false);\n document.removeEventListener('touchend', handleTouchEnd, false);\n };\n\n const onMouseDown = (event: MouseEvent) => {\n beginTracking();\n update(event);\n };\n\n const handleMouseMove = (event: MouseEvent) => {\n update(event);\n };\n\n const handleMouseUp = (event: MouseEvent) => {\n update(event, true);\n endTracking();\n };\n\n const handleTouchMove = (event: TouchEvent) => {\n event.preventDefault();\n update(event.touches[0] as any);\n };\n\n const handleTouchEnd = (event: TouchEvent) => {\n update(event.changedTouches[0] as any, true);\n endTracking();\n };\n\n const handleTouchStart = (event: TouchEvent) => {\n event.preventDefault();\n beginTracking();\n update(event.touches[0] as any);\n };\n\n node?.addEventListener('mousedown', onMouseDown);\n node?.addEventListener('touchstart', handleTouchStart, { passive: false });\n\n cleanupRef.current = () => {\n document.removeEventListener('mousemove', handleMouseMove, false);\n document.removeEventListener('mouseup', handleMouseUp, false);\n document.removeEventListener('touchmove', handleTouchMove, false);\n document.removeEventListener('touchend', handleTouchEnd, false);\n };\n\n return () => {\n if (node) {\n node.removeEventListener('mousedown', onMouseDown);\n node.removeEventListener('touchstart', handleTouchStart);\n }\n };\n },\n [onChange]\n );\n\n return { ref: refCallback, active };\n}\n\nexport namespace useRadialMove {\n export type Options = UseRadialMoveOptions;\n export type ReturnValue<T extends HTMLElement> = UseRadialMoveReturnValue<T>;\n}\n"],"mappings":"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"}
package/lib/index.d.mts CHANGED
@@ -79,6 +79,7 @@ export { useCollapse } from './use-collapse/use-collapse.js';
79
79
  export { useHorizontalCollapse } from './use-collapse/use-horizontal-collapse.js';
80
80
  export { useMask, formatMask, unformatMask, isMaskComplete, generatePattern, } from './use-mask/use-mask.js';
81
81
  export { useRovingIndex } from './use-roving-index/use-roving-index.js';
82
+ export { useDrag } from './use-drag/use-drag.js';
82
83
  export type { UseMediaQueryOptions } from './use-media-query/use-media-query';
83
84
  export type { UseClipboardInput as UseClipboardOptions, UseClipboardReturnValue, } from './use-clipboard/use-clipboard';
84
85
  export type { UseColorSchemeValue } from './use-color-scheme/use-color-scheme';
@@ -130,3 +131,4 @@ export type { UseCollapseInput, UseCollapseReturnValue, UseCollapseState, } from
130
131
  export type { UseHorizontalCollapseInput, UseHorizontalCollapseReturnValue, UseHorizontalCollapseState, } from './use-collapse/use-horizontal-collapse';
131
132
  export type { UseMaskOptions, UseMaskReturnValue, MaskState } from './use-mask/use-mask';
132
133
  export type { UseRovingIndexInput, UseRovingIndexGetItemPropsInput, UseRovingIndexReturnValue, } from './use-roving-index/use-roving-index';
134
+ export type { UseDragState, UseDragOptions, UseDragReturnValue } from './use-drag/use-drag';
package/lib/index.d.ts CHANGED
@@ -79,6 +79,7 @@ export { useCollapse } from './use-collapse/use-collapse.js';
79
79
  export { useHorizontalCollapse } from './use-collapse/use-horizontal-collapse.js';
80
80
  export { useMask, formatMask, unformatMask, isMaskComplete, generatePattern, } from './use-mask/use-mask.js';
81
81
  export { useRovingIndex } from './use-roving-index/use-roving-index.js';
82
+ export { useDrag } from './use-drag/use-drag.js';
82
83
  export type { UseMediaQueryOptions } from './use-media-query/use-media-query';
83
84
  export type { UseClipboardInput as UseClipboardOptions, UseClipboardReturnValue, } from './use-clipboard/use-clipboard';
84
85
  export type { UseColorSchemeValue } from './use-color-scheme/use-color-scheme';
@@ -130,3 +131,4 @@ export type { UseCollapseInput, UseCollapseReturnValue, UseCollapseState, } from
130
131
  export type { UseHorizontalCollapseInput, UseHorizontalCollapseReturnValue, UseHorizontalCollapseState, } from './use-collapse/use-horizontal-collapse';
131
132
  export type { UseMaskOptions, UseMaskReturnValue, MaskState } from './use-mask/use-mask';
132
133
  export type { UseRovingIndexInput, UseRovingIndexGetItemPropsInput, UseRovingIndexReturnValue, } from './use-roving-index/use-roving-index';
134
+ export type { UseDragState, UseDragOptions, UseDragReturnValue } from './use-drag/use-drag';
@@ -0,0 +1,60 @@
1
+ type Vector2 = [number, number];
2
+ export interface UseDragState {
3
+ /** Current pointer position [x, y] */
4
+ xy: Vector2;
5
+ /** Position where the gesture started [x, y] */
6
+ initial: Vector2;
7
+ /** Displacement from start [x, y], respects axis constraint */
8
+ movement: Vector2;
9
+ /** Change since previous event [x, y] */
10
+ delta: Vector2;
11
+ /** Absolute distance per axis [x, y] */
12
+ distance: Vector2;
13
+ /** Movement direction per axis: -1, 0, or 1 */
14
+ direction: Vector2;
15
+ /** Speed per axis in px/ms */
16
+ velocity: Vector2;
17
+ /** Time since drag started in ms */
18
+ elapsedTime: number;
19
+ /** `true` on the first handler call after the threshold is met */
20
+ first: boolean;
21
+ /** `true` on the last handler call (pointer released or canceled) */
22
+ last: boolean;
23
+ /** `true` while the gesture is ongoing */
24
+ active: boolean;
25
+ /** `true` when the gesture qualifies as a tap (requires `filterTaps: true`) */
26
+ tap: boolean;
27
+ /** `true` when the gesture was interrupted by a `pointercancel` event */
28
+ canceled: boolean;
29
+ /** Function to programmatically cancel the current gesture */
30
+ cancel: () => void;
31
+ /** The source `PointerEvent` */
32
+ event: PointerEvent;
33
+ }
34
+ export interface UseDragOptions {
35
+ /** Constrain movement to a specific axis. `'lock'` locks to whichever axis has more movement after `axisThreshold` is exceeded. */
36
+ axis?: 'x' | 'y' | 'lock';
37
+ /** Movement in px required to determine axis when `axis` is `'lock'`, `1` by default */
38
+ axisThreshold?: number;
39
+ /** When `true`, the last state includes `tap: true` when total distance is below `tapThreshold`, `false` by default */
40
+ filterTaps?: boolean;
41
+ /** Max displacement in px to still be considered a tap, `3` by default */
42
+ tapThreshold?: number;
43
+ /** Minimum displacement in px before the drag activates. Can be a number (both axes) or `[x, y]`. `0` by default */
44
+ threshold?: number | Vector2;
45
+ /** Enable or disable the hook, `true` by default */
46
+ enabled?: boolean;
47
+ }
48
+ export interface UseDragReturnValue<T extends HTMLElement = any> {
49
+ /** Ref callback to attach to the draggable element */
50
+ ref: React.RefCallback<T | null>;
51
+ /** `true` while the element is being dragged */
52
+ active: boolean;
53
+ }
54
+ export declare function useDrag<T extends HTMLElement = any>(handler: (state: UseDragState) => void, options?: UseDragOptions): UseDragReturnValue<T>;
55
+ export declare namespace useDrag {
56
+ type State = UseDragState;
57
+ type Options = UseDragOptions;
58
+ type ReturnValue<T extends HTMLElement = any> = UseDragReturnValue<T>;
59
+ }
60
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mantine/hooks",
3
- "version": "9.1.1",
3
+ "version": "9.2.0",
4
4
  "description": "A collection of 50+ hooks for state and UI management",
5
5
  "homepage": "https://mantine.dev",
6
6
  "license": "MIT",