diff --git a/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts b/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts
index 1dde45c74d..9a8d8fdaa4 100644
--- a/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts
+++ b/packages/framer-motion/src/gestures/drag/VisualElementDragControls.ts
@@ -657,7 +657,11 @@ export class VisualElementDragControls {
element,
"pointerdown",
(event) => {
- const { drag, dragListener = true } = this.getProps()
+ const {
+ drag,
+ dragListener = true,
+ dragSnapToCursor,
+ } = this.getProps()
const target = event.target as Element
/**
@@ -672,7 +676,7 @@ export class VisualElementDragControls {
target !== element && isElementTextInput(target)
if (drag && dragListener && !isClickingTextInputChild) {
- this.start(event)
+ this.start(event, { snapToCursor: dragSnapToCursor })
}
}
)
diff --git a/packages/framer-motion/src/gestures/drag/__tests__/use-drag-controls.test.tsx b/packages/framer-motion/src/gestures/drag/__tests__/use-drag-controls.test.tsx
index 54469272c1..52b06954a4 100644
--- a/packages/framer-motion/src/gestures/drag/__tests__/use-drag-controls.test.tsx
+++ b/packages/framer-motion/src/gestures/drag/__tests__/use-drag-controls.test.tsx
@@ -2,6 +2,7 @@ import { useState } from "react"
import { motion, useDragControls, DragControls, motionValue } from "../../../"
import { render } from "../../../jest.setup"
import { nextFrame } from "../../__tests__/utils"
+import { VisualElementDragControls } from "../VisualElementDragControls"
import { MockDrag, drag } from "./utils"
describe("useDragControls", () => {
@@ -209,3 +210,67 @@ describe("useDragControls", () => {
pointer2.end()
})
})
+
+describe("dragSnapToCursor prop", () => {
+ test("forwards snapToCursor option to start when prop is true", async () => {
+ const startSpy = jest.spyOn(
+ VisualElementDragControls.prototype,
+ "start"
+ )
+
+ const Component = () => (
+
+
+
+ )
+
+ const { rerender, getByTestId } = render()
+ rerender()
+
+ startSpy.mockClear()
+
+ const pointer = await drag(getByTestId("draggable")).to(50, 50)
+ pointer.end()
+ await nextFrame()
+
+ expect(startSpy).toHaveBeenCalled()
+ const [, options] = startSpy.mock.calls[0]
+ expect(options).toEqual(
+ expect.objectContaining({ snapToCursor: true })
+ )
+
+ startSpy.mockRestore()
+ })
+
+ test("does not forward snapToCursor option when prop is not set", async () => {
+ const startSpy = jest.spyOn(
+ VisualElementDragControls.prototype,
+ "start"
+ )
+
+ const Component = () => (
+
+
+
+ )
+
+ const { rerender, getByTestId } = render()
+ rerender()
+
+ startSpy.mockClear()
+
+ const pointer = await drag(getByTestId("draggable")).to(50, 50)
+ pointer.end()
+ await nextFrame()
+
+ expect(startSpy).toHaveBeenCalled()
+ const [, options] = startSpy.mock.calls[0]
+ expect(options?.snapToCursor).toBeFalsy()
+
+ startSpy.mockRestore()
+ })
+})
diff --git a/packages/motion-dom/src/node/types.ts b/packages/motion-dom/src/node/types.ts
index ee0164b448..a150fb6f23 100644
--- a/packages/motion-dom/src/node/types.ts
+++ b/packages/motion-dom/src/node/types.ts
@@ -733,6 +733,18 @@ export interface MotionNodeDraggableOptions {
*/
dragSnapToOrigin?: boolean | "x" | "y"
+ /**
+ * If `true`, the element will snap so that the cursor is centered on it
+ * when a drag gesture starts. This is the equivalent of passing
+ * `{ snapToCursor: true }` to a `dragControls.start()` call but works
+ * for any drag-enabled motion component.
+ *
+ * ```jsx
+ *
+ * ```
+ */
+ dragSnapToCursor?: boolean
+
/**
* By default, if `drag` is defined on a component then an event listener will be attached
* to automatically initiate dragging when a user presses down on it.