@kitlangton/solid-motion 0.1.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.
Files changed (135) hide show
  1. package/LICENSE +48 -0
  2. package/README.md +54 -0
  3. package/dist/animation/animate/index.d.ts +23 -0
  4. package/dist/animation/animate/resolve-subjects.d.ts +3 -0
  5. package/dist/animation/animate/sequence.d.ts +3 -0
  6. package/dist/animation/animate/subject.d.ts +7 -0
  7. package/dist/animation/sequence/create.d.ts +6 -0
  8. package/dist/animation/sequence/types.d.ts +51 -0
  9. package/dist/animation/sequence/utils/calc-repeat-duration.d.ts +1 -0
  10. package/dist/animation/sequence/utils/calc-time.d.ts +2 -0
  11. package/dist/animation/sequence/utils/edit.d.ts +5 -0
  12. package/dist/animation/sequence/utils/normalize-times.d.ts +1 -0
  13. package/dist/animation/sequence/utils/sort.d.ts +2 -0
  14. package/dist/animation/utils/create-visual-element.d.ts +2 -0
  15. package/dist/animation/utils/is-dom-keyframes.d.ts +2 -0
  16. package/dist/components/AnimatePresence.d.ts +27 -0
  17. package/dist/components/AnimatePresenceContext.d.ts +23 -0
  18. package/dist/components/AnimatePresenceTypes.d.ts +57 -0
  19. package/dist/components/MotionConfig.d.ts +16 -0
  20. package/dist/context/MotionContext/create.d.ts +3 -0
  21. package/dist/context/MotionContext/utils.d.ts +3 -0
  22. package/dist/context/MotionContext.d.ts +7 -0
  23. package/dist/context/PresenceContext.d.ts +13 -0
  24. package/dist/events/add-pointer-event.d.ts +2 -0
  25. package/dist/events/event-info.d.ts +4 -0
  26. package/dist/gestures/drag/VisualElementDragControls.d.ts +86 -0
  27. package/dist/gestures/drag/index.d.ts +11 -0
  28. package/dist/gestures/drag/utils/constraints.d.ts +77 -0
  29. package/dist/gestures/focus.d.ts +8 -0
  30. package/dist/gestures/hover.d.ts +5 -0
  31. package/dist/gestures/pan/PanSession.d.ts +102 -0
  32. package/dist/gestures/pan/index.d.ts +15 -0
  33. package/dist/gestures/press.d.ts +5 -0
  34. package/dist/index.d.ts +13 -0
  35. package/dist/index.js +2466 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/motion/features/animation/exit.d.ts +7 -0
  38. package/dist/motion/features/animation/index.d.ts +9 -0
  39. package/dist/motion/features/animations.d.ts +10 -0
  40. package/dist/motion/features/definitions.d.ts +3 -0
  41. package/dist/motion/features/feature-bundle.d.ts +34 -0
  42. package/dist/motion/features/gestures.d.ts +26 -0
  43. package/dist/motion/features/load-features.d.ts +1 -0
  44. package/dist/motion/index.d.ts +22 -0
  45. package/dist/motion/types.d.ts +27 -0
  46. package/dist/motion/utils/drag-projection.d.ts +3 -0
  47. package/dist/motion/utils/use-visual-state.d.ts +14 -0
  48. package/dist/motion/utils/valid-prop.d.ts +1 -0
  49. package/dist/render/dom/create-visual-element.d.ts +2 -0
  50. package/dist/render/dom/filter-props.d.ts +2 -0
  51. package/dist/render/dom/is-svg-component.d.ts +1 -0
  52. package/dist/render/dom/style.d.ts +1 -0
  53. package/dist/render/html/use-props.d.ts +7 -0
  54. package/dist/render/html/utils/create-render-state.d.ts +2 -0
  55. package/dist/render/svg/lowercase-elements.d.ts +1 -0
  56. package/dist/render/svg/use-props.d.ts +3 -0
  57. package/dist/render/svg/utils/create-render-state.d.ts +2 -0
  58. package/dist/render/types.d.ts +3 -0
  59. package/dist/server.js +2407 -0
  60. package/dist/server.js.map +1 -0
  61. package/dist/source/animation/animate/index.js +39 -0
  62. package/dist/source/animation/animate/resolve-subjects.js +19 -0
  63. package/dist/source/animation/animate/sequence.js +11 -0
  64. package/dist/source/animation/animate/subject.js +37 -0
  65. package/dist/source/animation/sequence/create.js +171 -0
  66. package/dist/source/animation/sequence/types.js +1 -0
  67. package/dist/source/animation/sequence/utils/calc-repeat-duration.js +3 -0
  68. package/dist/source/animation/sequence/utils/calc-time.js +17 -0
  69. package/dist/source/animation/sequence/utils/edit.js +21 -0
  70. package/dist/source/animation/sequence/utils/normalize-times.js +5 -0
  71. package/dist/source/animation/sequence/utils/sort.js +12 -0
  72. package/dist/source/animation/utils/create-visual-element.js +37 -0
  73. package/dist/source/animation/utils/is-dom-keyframes.js +3 -0
  74. package/dist/source/components/AnimatePresence.jsx +444 -0
  75. package/dist/source/components/AnimatePresenceContext.jsx +20 -0
  76. package/dist/source/components/AnimatePresenceTypes.js +1 -0
  77. package/dist/source/components/MotionConfig.jsx +13 -0
  78. package/dist/source/context/MotionContext/create.js +19 -0
  79. package/dist/source/context/MotionContext/utils.js +11 -0
  80. package/dist/source/context/MotionContext.js +2 -0
  81. package/dist/source/context/PresenceContext.js +2 -0
  82. package/dist/source/events/add-pointer-event.js +5 -0
  83. package/dist/source/events/event-info.js +16 -0
  84. package/dist/source/gestures/drag/VisualElementDragControls.js +531 -0
  85. package/dist/source/gestures/drag/index.js +47 -0
  86. package/dist/source/gestures/drag/utils/constraints.js +160 -0
  87. package/dist/source/gestures/focus.js +37 -0
  88. package/dist/source/gestures/hover.js +25 -0
  89. package/dist/source/gestures/pan/PanSession.js +273 -0
  90. package/dist/source/gestures/pan/index.js +46 -0
  91. package/dist/source/gestures/press.js +28 -0
  92. package/dist/source/index.js +11 -0
  93. package/dist/source/motion/features/animation/exit.js +33 -0
  94. package/dist/source/motion/features/animation/index.js +27 -0
  95. package/dist/source/motion/features/animations.js +10 -0
  96. package/dist/source/motion/features/definitions.js +29 -0
  97. package/dist/source/motion/features/feature-bundle.js +6 -0
  98. package/dist/source/motion/features/gestures.js +20 -0
  99. package/dist/source/motion/features/load-features.js +12 -0
  100. package/dist/source/motion/index.jsx +176 -0
  101. package/dist/source/motion/types.js +1 -0
  102. package/dist/source/motion/utils/drag-projection.js +16 -0
  103. package/dist/source/motion/utils/use-visual-state.js +53 -0
  104. package/dist/source/motion/utils/valid-prop.js +40 -0
  105. package/dist/source/render/dom/create-visual-element.js +10 -0
  106. package/dist/source/render/dom/filter-props.js +15 -0
  107. package/dist/source/render/dom/is-svg-component.js +10 -0
  108. package/dist/source/render/dom/style.js +12 -0
  109. package/dist/source/render/html/use-props.js +39 -0
  110. package/dist/source/render/html/utils/create-render-state.js +6 -0
  111. package/dist/source/render/svg/lowercase-elements.js +27 -0
  112. package/dist/source/render/svg/use-props.js +28 -0
  113. package/dist/source/render/svg/utils/create-render-state.js +5 -0
  114. package/dist/source/render/types.js +1 -0
  115. package/dist/source/utils/distance.js +7 -0
  116. package/dist/source/utils/get-context-window.js +4 -0
  117. package/dist/source/utils/is-ref-object.js +5 -0
  118. package/dist/source/utils/safe-react-types.js +1 -0
  119. package/dist/source/utils/use-reduced-motion.js +15 -0
  120. package/dist/source/value/use-motion-value-event.js +5 -0
  121. package/dist/source/value/use-motion-value.js +16 -0
  122. package/dist/source/value/use-spring.js +59 -0
  123. package/dist/source/value/use-transform.js +25 -0
  124. package/dist/source/value/use-velocity.js +17 -0
  125. package/dist/utils/distance.d.ts +3 -0
  126. package/dist/utils/get-context-window.d.ts +2 -0
  127. package/dist/utils/is-ref-object.d.ts +2 -0
  128. package/dist/utils/safe-react-types.d.ts +3 -0
  129. package/dist/utils/use-reduced-motion.d.ts +6 -0
  130. package/dist/value/use-motion-value-event.d.ts +3 -0
  131. package/dist/value/use-motion-value.d.ts +5 -0
  132. package/dist/value/use-spring.d.ts +5 -0
  133. package/dist/value/use-transform.d.ts +9 -0
  134. package/dist/value/use-velocity.d.ts +3 -0
  135. package/package.json +61 -0
@@ -0,0 +1,531 @@
1
+ import { addValueToWillChange, animateMotionValue, calcLength, convertBoundingBoxToBox, convertBoxToBoundingBox, createBox, eachAxis, frame, cancelFrame, measurePageBox, mixNumber, percent, setDragLock, } from "motion-dom";
2
+ import { invariant } from "motion-utils";
3
+ import { addDomEvent } from "motion-dom";
4
+ import { addPointerEvent } from "../../events/add-pointer-event";
5
+ import { extractEventInfo } from "../../events/event-info";
6
+ import { getContextWindow } from "../../utils/get-context-window";
7
+ import { isRefObject } from "../../utils/is-ref-object";
8
+ import { PanSession } from "../pan/PanSession";
9
+ import { applyConstraints, calcOrigin, calcRelativeConstraints, calcViewportConstraints, defaultElastic, rebaseAxisConstraints, resolveDragElastic, } from "./utils/constraints";
10
+ export const elementDragControls = new WeakMap();
11
+ export class VisualElementDragControls {
12
+ constructor(visualElement) {
13
+ this.openDragLock = null;
14
+ this.isDragging = false;
15
+ this.currentDirection = null;
16
+ this.originPoint = { x: 0, y: 0 };
17
+ /**
18
+ * The permitted boundaries of travel, in pixels.
19
+ */
20
+ this.constraints = false;
21
+ this.hasMutatedConstraints = false;
22
+ /**
23
+ * The per-axis resolved elastic values.
24
+ */
25
+ this.elastic = createBox();
26
+ /**
27
+ * The latest pointer event. Used as fallback when the `cancel` and `stop` functions are called without arguments.
28
+ */
29
+ this.latestPointerEvent = null;
30
+ /**
31
+ * The latest pan info. Used as fallback when the `cancel` and `stop` functions are called without arguments.
32
+ */
33
+ this.latestPanInfo = null;
34
+ this.visualElement = visualElement;
35
+ }
36
+ start(originEvent, { snapToCursor = false, distanceThreshold } = {}) {
37
+ /**
38
+ * Don't start dragging if this component is exiting
39
+ */
40
+ const { presenceContext } = this.visualElement;
41
+ if (presenceContext && presenceContext.isPresent === false)
42
+ return;
43
+ const onSessionStart = (event) => {
44
+ // Stop or pause animations based on context:
45
+ // - snapToCursor: stop because we'll set new position values
46
+ // - otherwise: pause to allow resume if no drag starts (for constraint animations)
47
+ if (snapToCursor) {
48
+ this.stopAnimation();
49
+ this.snapToCursor(extractEventInfo(event).point);
50
+ }
51
+ else {
52
+ this.pauseAnimation();
53
+ }
54
+ };
55
+ const onStart = (event, info) => {
56
+ // Stop any paused animation so motion values reflect true current position
57
+ // (pauseAnimation was called in onSessionStart to allow resume if no drag started)
58
+ this.stopAnimation();
59
+ // Attempt to grab the global drag gesture lock - maybe make this part of PanSession
60
+ const { drag, dragPropagation, onDragStart } = this.getProps();
61
+ if (drag && !dragPropagation) {
62
+ if (this.openDragLock)
63
+ this.openDragLock();
64
+ this.openDragLock = setDragLock(drag);
65
+ // If we don 't have the lock, don't start dragging
66
+ if (!this.openDragLock)
67
+ return;
68
+ }
69
+ this.latestPointerEvent = event;
70
+ this.latestPanInfo = info;
71
+ this.isDragging = true;
72
+ this.currentDirection = null;
73
+ this.resolveConstraints();
74
+ if (this.visualElement.projection) {
75
+ this.visualElement.projection.isAnimationBlocked = true;
76
+ this.visualElement.projection.target = undefined;
77
+ }
78
+ /**
79
+ * Record gesture origin and pointer offset
80
+ */
81
+ eachAxis((axis) => {
82
+ let current = this.getAxisMotionValue(axis).get() || 0;
83
+ /**
84
+ * If the MotionValue is a percentage value convert to px
85
+ */
86
+ if (percent.test(current)) {
87
+ const { projection } = this.visualElement;
88
+ if (projection && projection.layout) {
89
+ const measuredAxis = projection.layout.layoutBox[axis];
90
+ if (measuredAxis) {
91
+ const length = calcLength(measuredAxis);
92
+ current = length * (parseFloat(current) / 100);
93
+ }
94
+ }
95
+ }
96
+ this.originPoint[axis] = current;
97
+ });
98
+ // Fire onDragStart event
99
+ if (onDragStart) {
100
+ frame.postRender(() => onDragStart(event, info));
101
+ }
102
+ addValueToWillChange(this.visualElement, "transform");
103
+ const { animationState } = this.visualElement;
104
+ animationState && animationState.setActive("whileDrag", true);
105
+ };
106
+ const onMove = (event, info) => {
107
+ this.latestPointerEvent = event;
108
+ this.latestPanInfo = info;
109
+ const { dragPropagation, dragDirectionLock, onDirectionLock, onDrag, } = this.getProps();
110
+ // If we didn't successfully receive the gesture lock, early return.
111
+ if (!dragPropagation && !this.openDragLock)
112
+ return;
113
+ const { offset } = info;
114
+ // Attempt to detect drag direction if directionLock is true
115
+ if (dragDirectionLock && this.currentDirection === null) {
116
+ this.currentDirection = getCurrentDirection(offset);
117
+ // If we've successfully set a direction, notify listener
118
+ if (this.currentDirection !== null) {
119
+ onDirectionLock && onDirectionLock(this.currentDirection);
120
+ }
121
+ return;
122
+ }
123
+ // Update each point with the latest position
124
+ this.updateAxis("x", info.point, offset);
125
+ this.updateAxis("y", info.point, offset);
126
+ /**
127
+ * Ideally we would leave the renderer to fire naturally at the end of
128
+ * this frame but if the element is about to change layout as the result
129
+ * of a re-render we want to ensure the browser can read the latest
130
+ * bounding box to ensure the pointer and element don't fall out of sync.
131
+ */
132
+ this.visualElement.render();
133
+ /**
134
+ * This must fire after the render call as it might trigger a state
135
+ * change which itself might trigger a layout update.
136
+ */
137
+ onDrag && onDrag(event, info);
138
+ };
139
+ const onSessionEnd = (event, info) => {
140
+ this.latestPointerEvent = event;
141
+ this.latestPanInfo = info;
142
+ this.stop(event, info);
143
+ this.latestPointerEvent = null;
144
+ this.latestPanInfo = null;
145
+ };
146
+ const resumeAnimation = () => eachAxis((axis) => this.getAnimationState(axis) === "paused" &&
147
+ this.getAxisMotionValue(axis).animation?.play());
148
+ const { dragSnapToOrigin } = this.getProps();
149
+ this.panSession = new PanSession(originEvent, {
150
+ onSessionStart,
151
+ onStart,
152
+ onMove,
153
+ onSessionEnd,
154
+ resumeAnimation,
155
+ }, {
156
+ transformPagePoint: this.visualElement.getTransformPagePoint(),
157
+ dragSnapToOrigin,
158
+ distanceThreshold,
159
+ contextWindow: getContextWindow(this.visualElement),
160
+ element: this.visualElement.current,
161
+ });
162
+ }
163
+ /**
164
+ * @internal
165
+ */
166
+ stop(event, panInfo) {
167
+ const finalEvent = event || this.latestPointerEvent;
168
+ const finalPanInfo = panInfo || this.latestPanInfo;
169
+ const isDragging = this.isDragging;
170
+ this.cancel();
171
+ if (!isDragging || !finalPanInfo || !finalEvent)
172
+ return;
173
+ const { velocity } = finalPanInfo;
174
+ this.startAnimation(velocity);
175
+ const { onDragEnd } = this.getProps();
176
+ if (onDragEnd) {
177
+ frame.postRender(() => onDragEnd(finalEvent, finalPanInfo));
178
+ }
179
+ }
180
+ /**
181
+ * @internal
182
+ */
183
+ cancel() {
184
+ this.isDragging = false;
185
+ const { projection, animationState } = this.visualElement;
186
+ if (projection) {
187
+ projection.isAnimationBlocked = false;
188
+ }
189
+ this.endPanSession();
190
+ const { dragPropagation } = this.getProps();
191
+ if (!dragPropagation && this.openDragLock) {
192
+ this.openDragLock();
193
+ this.openDragLock = null;
194
+ }
195
+ animationState && animationState.setActive("whileDrag", false);
196
+ }
197
+ /**
198
+ * Clean up the pan session without modifying other drag state.
199
+ * This is used during unmount to ensure event listeners are removed
200
+ * without affecting projection animations or drag locks.
201
+ * @internal
202
+ */
203
+ endPanSession() {
204
+ this.panSession && this.panSession.end();
205
+ this.panSession = undefined;
206
+ }
207
+ /** Final owner disposal, including any release inertia on external values. */
208
+ dispose() {
209
+ this.cancel();
210
+ this.stopAnimation();
211
+ elementDragControls.delete(this.visualElement);
212
+ }
213
+ updateAxis(axis, _point, offset) {
214
+ const { drag } = this.getProps();
215
+ // If we're not dragging this axis, do an early return.
216
+ if (!offset || !shouldDrag(axis, drag, this.currentDirection))
217
+ return;
218
+ const axisValue = this.getAxisMotionValue(axis);
219
+ let next = this.originPoint[axis] + offset[axis];
220
+ // Apply constraints
221
+ if (this.constraints && this.constraints[axis]) {
222
+ next = applyConstraints(next, this.constraints[axis], this.elastic[axis]);
223
+ }
224
+ axisValue.set(next);
225
+ }
226
+ resolveConstraints() {
227
+ const { dragConstraints, dragElastic } = this.getProps();
228
+ const layout = this.visualElement.projection &&
229
+ !this.visualElement.projection.layout
230
+ ? this.visualElement.projection.measure(false)
231
+ : this.visualElement.projection?.layout;
232
+ const prevConstraints = this.constraints;
233
+ if (dragConstraints && isRefObject(dragConstraints)) {
234
+ if (!this.constraints) {
235
+ this.constraints = this.resolveRefConstraints();
236
+ }
237
+ }
238
+ else {
239
+ if (dragConstraints && layout) {
240
+ this.constraints = calcRelativeConstraints(layout.layoutBox, dragConstraints);
241
+ }
242
+ else {
243
+ this.constraints = false;
244
+ }
245
+ }
246
+ this.elastic = resolveDragElastic(dragElastic);
247
+ /**
248
+ * If we're outputting to external MotionValues, we want to rebase the measured constraints
249
+ * from viewport-relative to component-relative.
250
+ */
251
+ if (prevConstraints !== this.constraints &&
252
+ layout &&
253
+ this.constraints &&
254
+ !this.hasMutatedConstraints) {
255
+ eachAxis((axis) => {
256
+ if (this.constraints !== false &&
257
+ this.getAxisMotionValue(axis)) {
258
+ this.constraints[axis] = rebaseAxisConstraints(layout.layoutBox[axis], this.constraints[axis]);
259
+ }
260
+ });
261
+ }
262
+ }
263
+ resolveRefConstraints() {
264
+ const { dragConstraints: constraints, onMeasureDragConstraints } = this.getProps();
265
+ if (!constraints || !isRefObject(constraints))
266
+ return false;
267
+ const constraintsElement = constraints.current;
268
+ invariant(constraintsElement !== null, "If `dragConstraints` is set as a React ref, that ref must be passed to another component's `ref` prop.", "drag-constraints-ref");
269
+ const { projection } = this.visualElement;
270
+ // TODO
271
+ if (!projection || !projection.layout)
272
+ return false;
273
+ const constraintsBox = measurePageBox(constraintsElement, projection.root, this.visualElement.getTransformPagePoint());
274
+ let measuredConstraints = calcViewportConstraints(projection.layout.layoutBox, constraintsBox);
275
+ /**
276
+ * If there's an onMeasureDragConstraints listener we call it and
277
+ * if different constraints are returned, set constraints to that
278
+ */
279
+ if (onMeasureDragConstraints) {
280
+ const userConstraints = onMeasureDragConstraints(convertBoxToBoundingBox(measuredConstraints));
281
+ this.hasMutatedConstraints = !!userConstraints;
282
+ if (userConstraints) {
283
+ measuredConstraints = convertBoundingBoxToBox(userConstraints);
284
+ }
285
+ }
286
+ return measuredConstraints;
287
+ }
288
+ startAnimation(velocity) {
289
+ const { drag, dragMomentum, dragElastic, dragTransition, dragSnapToOrigin, onDragTransitionEnd, } = this.getProps();
290
+ const constraints = this.constraints || {};
291
+ const momentumAnimations = eachAxis((axis) => {
292
+ if (!shouldDrag(axis, drag, this.currentDirection)) {
293
+ return;
294
+ }
295
+ let transition = (constraints && constraints[axis]) || {};
296
+ if (dragSnapToOrigin)
297
+ transition = { min: 0, max: 0 };
298
+ /**
299
+ * Overdamp the boundary spring if `dragElastic` is disabled. There's still a frame
300
+ * of spring animations so we should look into adding a disable spring option to `inertia`.
301
+ * We could do something here where we affect the `bounceStiffness` and `bounceDamping`
302
+ * using the value of `dragElastic`.
303
+ */
304
+ const bounceStiffness = dragElastic ? 200 : 1000000;
305
+ const bounceDamping = dragElastic ? 40 : 10000000;
306
+ const inertia = {
307
+ type: "inertia",
308
+ velocity: dragMomentum ? velocity[axis] : 0,
309
+ bounceStiffness,
310
+ bounceDamping,
311
+ timeConstant: 750,
312
+ restDelta: 1,
313
+ restSpeed: 10,
314
+ ...dragTransition,
315
+ ...transition,
316
+ };
317
+ // If we're not animating on an externally-provided `MotionValue` we can use the
318
+ // component's animation controls which will handle interactions with whileHover (etc),
319
+ // otherwise we just have to animate the `MotionValue` itself.
320
+ return this.startAxisValueAnimation(axis, inertia);
321
+ });
322
+ // Run all animations and then resolve the new drag constraints.
323
+ return Promise.all(momentumAnimations).then(onDragTransitionEnd);
324
+ }
325
+ startAxisValueAnimation(axis, transition) {
326
+ const axisValue = this.getAxisMotionValue(axis);
327
+ addValueToWillChange(this.visualElement, axis);
328
+ return axisValue.start(animateMotionValue(axis, axisValue, 0, transition, this.visualElement, false));
329
+ }
330
+ stopAnimation() {
331
+ eachAxis((axis) => this.getAxisMotionValue(axis).stop());
332
+ }
333
+ pauseAnimation() {
334
+ eachAxis((axis) => this.getAxisMotionValue(axis).animation?.pause());
335
+ }
336
+ getAnimationState(axis) {
337
+ return this.getAxisMotionValue(axis).animation?.state;
338
+ }
339
+ /**
340
+ * Drag works differently depending on which props are provided.
341
+ *
342
+ * - If _dragX and _dragY are provided, we output the gesture delta directly to those motion values.
343
+ * - Otherwise, we apply the delta to the x/y motion values.
344
+ */
345
+ getAxisMotionValue(axis) {
346
+ const dragKey = `_drag${axis.toUpperCase()}`;
347
+ const props = this.visualElement.getProps();
348
+ const externalMotionValue = props[dragKey];
349
+ return externalMotionValue
350
+ ? externalMotionValue
351
+ : this.visualElement.getValue(axis, (props.initial
352
+ ? props.initial[axis]
353
+ : undefined) || 0);
354
+ }
355
+ snapToCursor(point) {
356
+ eachAxis((axis) => {
357
+ const { drag } = this.getProps();
358
+ // If we're not dragging this axis, do an early return.
359
+ if (!shouldDrag(axis, drag, this.currentDirection))
360
+ return;
361
+ const { projection } = this.visualElement;
362
+ const axisValue = this.getAxisMotionValue(axis);
363
+ if (projection && projection.layout) {
364
+ const { min, max } = projection.layout.layoutBox[axis];
365
+ /**
366
+ * The layout measurement includes the current transform value,
367
+ * so we need to add it back to get the correct snap position.
368
+ * This fixes an issue where elements with initial coordinates
369
+ * would snap to the wrong position on the first drag.
370
+ */
371
+ const current = axisValue.get() || 0;
372
+ axisValue.set(point[axis] - mixNumber(min, max, 0.5) + current);
373
+ }
374
+ });
375
+ }
376
+ /**
377
+ * When the viewport resizes we want to check if the measured constraints
378
+ * have changed and, if so, reposition the element within those new constraints
379
+ * relative to where it was before the resize.
380
+ */
381
+ scalePositionWithinConstraints() {
382
+ if (!this.visualElement.current)
383
+ return;
384
+ const { drag, dragConstraints } = this.getProps();
385
+ const { projection } = this.visualElement;
386
+ if (!isRefObject(dragConstraints) || !projection || !this.constraints)
387
+ return;
388
+ /**
389
+ * Stop current animations as there can be visual glitching if we try to do
390
+ * this mid-animation
391
+ */
392
+ this.stopAnimation();
393
+ /**
394
+ * Record the relative position of the dragged element relative to the
395
+ * constraints box and save as a progress value.
396
+ */
397
+ const boxProgress = { x: 0, y: 0 };
398
+ eachAxis((axis) => {
399
+ const axisValue = this.getAxisMotionValue(axis);
400
+ if (axisValue && this.constraints !== false) {
401
+ const latest = axisValue.get();
402
+ boxProgress[axis] = calcOrigin({ min: latest, max: latest }, this.constraints[axis]);
403
+ }
404
+ });
405
+ /**
406
+ * Update the layout of this element and resolve the latest drag constraints
407
+ */
408
+ const { transformTemplate } = this.visualElement.getProps();
409
+ this.visualElement.current.style.transform = transformTemplate
410
+ ? transformTemplate({}, "")
411
+ : "none";
412
+ projection.root && projection.root.updateScroll();
413
+ projection.updateLayout();
414
+ this.resolveConstraints();
415
+ /**
416
+ * For each axis, calculate the current progress of the layout axis
417
+ * within the new constraints.
418
+ */
419
+ eachAxis((axis) => {
420
+ if (!shouldDrag(axis, drag, null))
421
+ return;
422
+ /**
423
+ * Calculate a new transform based on the previous box progress
424
+ */
425
+ const axisValue = this.getAxisMotionValue(axis);
426
+ const { min, max } = this.constraints[axis];
427
+ axisValue.set(mixNumber(min, max, boxProgress[axis]));
428
+ });
429
+ }
430
+ addListeners() {
431
+ if (!this.visualElement.current)
432
+ return;
433
+ elementDragControls.set(this.visualElement, this);
434
+ const element = this.visualElement.current;
435
+ /**
436
+ * Attach a pointerdown event listener on this DOM element to initiate drag tracking.
437
+ */
438
+ const stopPointerListener = addPointerEvent(element, "pointerdown", (event) => {
439
+ const { drag, dragListener = true } = this.getProps();
440
+ const target = event.target;
441
+ /**
442
+ * Allow dragging from buttons/links (e.g. carousel items), while
443
+ * preserving text editing and native input interactions.
444
+ */
445
+ const isClickingTextInputChild = target !== element &&
446
+ (Boolean(target.closest("input, textarea, select")) ||
447
+ (target instanceof HTMLElement && target.isContentEditable));
448
+ if (drag && dragListener && !isClickingTextInputChild) {
449
+ this.start(event);
450
+ }
451
+ });
452
+ const measureDragConstraints = () => {
453
+ const { dragConstraints } = this.getProps();
454
+ if (isRefObject(dragConstraints) && dragConstraints.current) {
455
+ this.constraints = this.resolveRefConstraints();
456
+ }
457
+ };
458
+ const { projection } = this.visualElement;
459
+ const stopMeasureLayoutListener = projection.addEventListener("measure", measureDragConstraints);
460
+ if (projection && !projection.layout) {
461
+ projection.root && projection.root.updateScroll();
462
+ projection.updateLayout();
463
+ }
464
+ frame.read(measureDragConstraints);
465
+ /**
466
+ * Attach a window resize listener to scale the draggable target within its defined
467
+ * constraints as the window resizes.
468
+ */
469
+ const stopResizeListener = addDomEvent(window, "resize", () => this.scalePositionWithinConstraints());
470
+ /**
471
+ * If the element's layout changes, calculate the delta and apply that to
472
+ * the drag gesture's origin point.
473
+ */
474
+ const stopLayoutUpdateListener = projection.addEventListener("didUpdate", (({ delta, hasLayoutChanged }) => {
475
+ if (this.isDragging && hasLayoutChanged) {
476
+ eachAxis((axis) => {
477
+ const motionValue = this.getAxisMotionValue(axis);
478
+ if (!motionValue)
479
+ return;
480
+ this.originPoint[axis] += delta[axis].translate;
481
+ motionValue.set(motionValue.get() + delta[axis].translate);
482
+ });
483
+ this.visualElement.render();
484
+ }
485
+ }));
486
+ return () => {
487
+ cancelFrame(measureDragConstraints);
488
+ stopResizeListener();
489
+ stopPointerListener();
490
+ stopMeasureLayoutListener();
491
+ stopLayoutUpdateListener && stopLayoutUpdateListener();
492
+ };
493
+ }
494
+ getProps() {
495
+ const props = this.visualElement.getProps();
496
+ const { drag = false, dragDirectionLock = false, dragPropagation = false, dragConstraints = false, dragElastic = defaultElastic, dragMomentum = true, } = props;
497
+ return {
498
+ ...props,
499
+ drag,
500
+ dragDirectionLock,
501
+ dragPropagation,
502
+ dragConstraints,
503
+ dragElastic,
504
+ dragMomentum,
505
+ };
506
+ }
507
+ }
508
+ function shouldDrag(direction, drag, currentDirection) {
509
+ return ((drag === true || drag === direction) &&
510
+ (currentDirection === null || currentDirection === direction));
511
+ }
512
+ /**
513
+ * Based on an x/y offset determine the current drag direction. If both axis' offsets are lower
514
+ * than the provided threshold, return `null`.
515
+ *
516
+ * @param offset - The x/y offset from origin.
517
+ * @param lockThreshold - (Optional) - the minimum absolute offset before we can determine a drag direction.
518
+ */
519
+ function getCurrentDirection(offset, lockThreshold = 10) {
520
+ let direction = null;
521
+ if (Math.abs(offset.y) > lockThreshold) {
522
+ direction = "y";
523
+ }
524
+ else if (Math.abs(offset.x) > lockThreshold) {
525
+ direction = "x";
526
+ }
527
+ return direction;
528
+ }
529
+ export function expectsResolvedDragConstraints({ dragConstraints, onMeasureDragConstraints, }) {
530
+ return isRefObject(dragConstraints) && !!onMeasureDragConstraints;
531
+ }
@@ -0,0 +1,47 @@
1
+ import { Feature } from "motion-dom";
2
+ import { noop } from "motion-utils";
3
+ import { VisualElementDragControls } from "./VisualElementDragControls";
4
+ export class DragGesture extends Feature {
5
+ constructor(node) {
6
+ super(node);
7
+ this.removeGroupControls = noop;
8
+ this.removeListeners = noop;
9
+ this.controls = new VisualElementDragControls(node);
10
+ }
11
+ mount() {
12
+ // If we've been provided a DragControls for manual control over the drag gesture,
13
+ // subscribe this component to it on mount.
14
+ const { dragControls } = this.node.getProps();
15
+ if (dragControls) {
16
+ this.removeGroupControls = dragControls.subscribe(this.controls);
17
+ }
18
+ this.removeListeners = this.controls.addListeners() || noop;
19
+ }
20
+ update() {
21
+ const { dragControls } = this.node.getProps();
22
+ const { dragControls: prevDragControls } = this.node.prevProps || {};
23
+ if (dragControls !== prevDragControls) {
24
+ this.removeGroupControls();
25
+ if (dragControls) {
26
+ this.removeGroupControls = dragControls.subscribe(this.controls);
27
+ }
28
+ }
29
+ }
30
+ unmount() {
31
+ this.removeGroupControls();
32
+ this.removeListeners();
33
+ /**
34
+ * In React 19, during list reorder reconciliation, components may
35
+ * briefly unmount and remount while the drag is still active. If we're
36
+ * actively dragging, we should NOT end the pan session - it will
37
+ * continue tracking pointer events via its window-level listeners.
38
+ *
39
+ * The pan session will be properly cleaned up when:
40
+ * 1. The drag ends naturally (pointerup/pointercancel)
41
+ * 2. The component is truly removed from the DOM
42
+ */
43
+ if (!this.controls.isDragging) {
44
+ this.controls.endPanSession();
45
+ }
46
+ }
47
+ }