From d938ce13690bf8e3beab1ac900c5b56a895ce7f4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Jan 2026 10:04:26 +0000 Subject: [PATCH 01/11] Add sourcemap support to motion-dom and motion-utils packages Enable sourcemap generation in the CJS and ES output configurations for motion-dom and motion-utils by adding the sourcemaps plugin and output option. This fixes the "Can't resolve original location of error" warnings when building with tools like Vite that rely on sourcemaps. Fixes #3431 --- packages/motion-dom/rollup.config.mjs | 9 ++++++--- packages/motion-utils/rollup.config.mjs | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/motion-dom/rollup.config.mjs b/packages/motion-dom/rollup.config.mjs index 7d777ea298..55e83b9c6a 100644 --- a/packages/motion-dom/rollup.config.mjs +++ b/packages/motion-dom/rollup.config.mjs @@ -1,6 +1,7 @@ import resolve from "@rollup/plugin-node-resolve" import replace from "@rollup/plugin-replace" import terser from "@rollup/plugin-terser" +import sourcemaps from "rollup-plugin-sourcemaps" import dts from "rollup-plugin-dts" import preserveDirectives from "rollup-plugin-preserve-directives" import pkg from "./package.json" with { type: "json" } @@ -83,9 +84,10 @@ const cjs = Object.assign({}, config, { dir: "dist/cjs", format: "cjs", exports: "named", - esModule: true + esModule: true, + sourcemap: true, }, - plugins: [resolve(), replaceSettings()], + plugins: [resolve(), replaceSettings(), sourcemaps()], external, onwarn(warning, warn) { if (warning.code === 'MODULE_LEVEL_DIRECTIVE') { @@ -104,8 +106,9 @@ export const es = Object.assign({}, config, { exports: "named", preserveModules: true, dir: "dist/es", + sourcemap: true, }, - plugins: [resolve(), replaceSettings(), preserveDirectives()], + plugins: [resolve(), replaceSettings(), preserveDirectives(), sourcemaps()], external, onwarn(warning, warn) { if (warning.code === 'MODULE_LEVEL_DIRECTIVE') { diff --git a/packages/motion-utils/rollup.config.mjs b/packages/motion-utils/rollup.config.mjs index d6ce0d4ec4..172de1bd5d 100644 --- a/packages/motion-utils/rollup.config.mjs +++ b/packages/motion-utils/rollup.config.mjs @@ -1,6 +1,7 @@ import resolve from "@rollup/plugin-node-resolve" import replace from "@rollup/plugin-replace" import terser from "@rollup/plugin-terser" +import sourcemaps from "rollup-plugin-sourcemaps" import dts from "rollup-plugin-dts" import preserveDirectives from "rollup-plugin-preserve-directives" import pkg from "./package.json" with { type: "json" } @@ -80,9 +81,10 @@ const cjs = Object.assign({}, config, { dir: "dist/cjs", format: "cjs", exports: "named", - esModule: true + esModule: true, + sourcemap: true, }, - plugins: [resolve(), replaceSettings()], + plugins: [resolve(), replaceSettings(), sourcemaps()], external, onwarn(warning, warn) { if (warning.code === 'MODULE_LEVEL_DIRECTIVE') { @@ -101,8 +103,9 @@ export const es = Object.assign({}, config, { exports: "named", preserveModules: true, dir: "dist/es", + sourcemap: true, }, - plugins: [resolve(), replaceSettings(), preserveDirectives()], + plugins: [resolve(), replaceSettings(), preserveDirectives(), sourcemaps()], external, onwarn(warning, warn) { if (warning.code === 'MODULE_LEVEL_DIRECTIVE') { From 513898de68a1b45ce8c40f96986b596cd9c86a00 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Jan 2026 10:24:29 +0000 Subject: [PATCH 02/11] Fix Reorder autoscroll when page itself is scrollable When the Reorder component is used on a scrollable page (without an explicit overflow: auto/scroll container), the autoscroll feature was not working because findScrollableAncestor only looked for elements with overflow: auto/scroll CSS properties. This fix adds support for page-level scrolling by: 1. Detecting when the document itself is scrollable (scrollHeight > innerHeight) 2. Using window.scrollBy() for page-level scrolling when no scrollable ancestor is found 3. Calculating scroll thresholds based on viewport dimensions instead of element bounds Fixes #3469 --- .../src/tests/reorder-auto-scroll-page.tsx | 87 +++++++++ .../integration/reorder-auto-scroll.ts | 175 ++++++++++++------ .../components/Reorder/utils/auto-scroll.ts | 87 ++++++++- 3 files changed, 296 insertions(+), 53 deletions(-) create mode 100644 dev/react/src/tests/reorder-auto-scroll-page.tsx diff --git a/dev/react/src/tests/reorder-auto-scroll-page.tsx b/dev/react/src/tests/reorder-auto-scroll-page.tsx new file mode 100644 index 0000000000..5807375a46 --- /dev/null +++ b/dev/react/src/tests/reorder-auto-scroll-page.tsx @@ -0,0 +1,87 @@ +import * as React from "react" +import { useState } from "react" +import { Reorder, useMotionValue } from "framer-motion" + +const initialItems = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] + +interface ItemProps { + item: number +} + +const Item = ({ item }: ItemProps) => { + const y = useMotionValue(0) + const hue = item * 20 + + return ( + + ) +} + +/** + * Test case for auto-scroll when the page itself is scrollable. + * This differs from reorder-auto-scroll.tsx which uses a wrapper div + * with overflow: auto. Here, the document/body is the scroll container. + */ +export const App = () => { + const [items, setItems] = useState(initialItems) + + return ( + <> +
+ + {items.map((item) => ( + + ))} + +
+ + + ) +} + +const styles = ` +html, body { + width: 100%; + min-height: 100%; + background: #333; + padding: 0; + margin: 0; +} + +body { + padding: 20px; + box-sizing: border-box; +} + +ul, +li { + list-style: none; + padding: 0; + margin: 0; +} + +ul { + position: relative; + width: 300px; + margin: 0 auto; +} + +li { + border-radius: 10px; + margin-bottom: 10px; + width: 100%; + height: 60px; + position: relative; + border-radius: 5px; + flex-shrink: 0; + cursor: grab; +} +` diff --git a/packages/framer-motion/cypress/integration/reorder-auto-scroll.ts b/packages/framer-motion/cypress/integration/reorder-auto-scroll.ts index 08aea09d1c..6be867b02f 100644 --- a/packages/framer-motion/cypress/integration/reorder-auto-scroll.ts +++ b/packages/framer-motion/cypress/integration/reorder-auto-scroll.ts @@ -4,61 +4,132 @@ * container, the container automatically scrolls. */ describe("Reorder auto-scroll", () => { - it("Auto-scrolls down when dragging near bottom edge", () => { - cy.visit("?test=reorder-auto-scroll") - .wait(200) - .get("[data-testid='scroll-container']") - .then(($container) => { - const container = $container[0] - expect(container.scrollTop).to.equal(0) - }) - .get("[data-testid='0']") - .then(() => { - cy.get("[data-testid='scroll-container']").then(($container) => { - const containerRect = $container[0].getBoundingClientRect() - const nearBottom = containerRect.bottom - containerRect.top - 20 + describe("with scrollable container", () => { + it("Auto-scrolls down when dragging near bottom edge", () => { + cy.visit("?test=reorder-auto-scroll") + .wait(200) + .get("[data-testid='scroll-container']") + .then(($container) => { + const container = $container[0] + expect(container.scrollTop).to.equal(0) + }) + .get("[data-testid='0']") + .then(() => { + cy.get("[data-testid='scroll-container']").then( + ($container) => { + const containerRect = + $container[0].getBoundingClientRect() + const nearBottom = + containerRect.bottom - containerRect.top - 20 + + cy.get("[data-testid='0']") + .trigger("pointerdown", 50, 25) + .wait(50) + .trigger("pointermove", 50, 30, { force: true }) + .wait(50) + .trigger("pointermove", 50, nearBottom, { + force: true, + }) + .wait(300) + .get("[data-testid='scroll-container']") + .then(($c) => { + expect( + $c[0].scrollTop + ).to.be.greaterThan(0) + }) + .get("[data-testid='0']") + .trigger("pointerup", { force: true }) + } + ) + }) + }) - cy.get("[data-testid='0']") - .trigger("pointerdown", 50, 25) - .wait(50) - .trigger("pointermove", 50, 30, { force: true }) - .wait(50) - .trigger("pointermove", 50, nearBottom, { force: true }) - .wait(300) - .get("[data-testid='scroll-container']") - .then(($c) => { - expect($c[0].scrollTop).to.be.greaterThan(0) - }) - .get("[data-testid='0']") - .trigger("pointerup", { force: true }) + it("Auto-scrolls up when dragging near top edge", () => { + cy.visit("?test=reorder-auto-scroll") + .wait(200) + .get("[data-testid='scroll-container']") + .then(($container) => { + $container[0].scrollTop = 200 }) - }) + .wait(100) + .get("[data-testid='scroll-container']") + .should(($container) => { + expect($container[0].scrollTop).to.equal(200) + }) + .get("[data-testid='8']") + .trigger("pointerdown", 50, 25) + .wait(50) + .trigger("pointermove", 50, 20, { force: true }) + .wait(50) + .trigger("pointermove", 50, -100, { force: true }) + .wait(300) + .get("[data-testid='scroll-container']") + .then(($container) => { + expect($container[0].scrollTop).to.be.lessThan(200) + }) + .get("[data-testid='8']") + .trigger("pointerup", { force: true }) + }) }) - it("Auto-scrolls up when dragging near top edge", () => { - cy.visit("?test=reorder-auto-scroll") - .wait(200) - .get("[data-testid='scroll-container']") - .then(($container) => { - $container[0].scrollTop = 200 - }) - .wait(100) - .get("[data-testid='scroll-container']") - .should(($container) => { - expect($container[0].scrollTop).to.equal(200) - }) - .get("[data-testid='8']") - .trigger("pointerdown", 50, 25) - .wait(50) - .trigger("pointermove", 50, 20, { force: true }) - .wait(50) - .trigger("pointermove", 50, -100, { force: true }) - .wait(300) - .get("[data-testid='scroll-container']") - .then(($container) => { - expect($container[0].scrollTop).to.be.lessThan(200) - }) - .get("[data-testid='8']") - .trigger("pointerup", { force: true }) + describe("with page-level scrolling", () => { + it("Auto-scrolls down when dragging near bottom of viewport", () => { + cy.visit("?test=reorder-auto-scroll-page") + .wait(200) + .window() + .then((win) => { + expect(win.scrollY).to.equal(0) + }) + .get("[data-testid='0']") + .then(() => { + cy.window().then((win) => { + const viewportHeight = win.innerHeight + const nearBottom = viewportHeight - 20 + + cy.get("[data-testid='0']") + .trigger("pointerdown", 50, 25) + .wait(50) + .trigger("pointermove", 50, 30, { force: true }) + .wait(50) + .trigger("pointermove", 50, nearBottom, { + force: true, + }) + .wait(300) + .window() + .then((w) => { + expect(w.scrollY).to.be.greaterThan(0) + }) + .get("[data-testid='0']") + .trigger("pointerup", { force: true }) + }) + }) + }) + + it("Auto-scrolls up when dragging near top of viewport", () => { + cy.visit("?test=reorder-auto-scroll-page") + .wait(200) + .window() + .then((win) => { + win.scrollTo(0, 200) + }) + .wait(100) + .window() + .should((win) => { + expect(win.scrollY).to.equal(200) + }) + .get("[data-testid='8']") + .trigger("pointerdown", 50, 25) + .wait(50) + .trigger("pointermove", 50, 20, { force: true }) + .wait(50) + .trigger("pointermove", 50, 10, { force: true }) + .wait(300) + .window() + .then((win) => { + expect(win.scrollY).to.be.lessThan(200) + }) + .get("[data-testid='8']") + .trigger("pointerup", { force: true }) + }) }) }) diff --git a/packages/framer-motion/src/components/Reorder/utils/auto-scroll.ts b/packages/framer-motion/src/components/Reorder/utils/auto-scroll.ts index b77460a9e3..692fa1f2e3 100644 --- a/packages/framer-motion/src/components/Reorder/utils/auto-scroll.ts +++ b/packages/framer-motion/src/components/Reorder/utils/auto-scroll.ts @@ -10,10 +10,18 @@ const initialScrollLimits = new WeakMap() type ActiveEdge = "start" | "end" | null const activeScrollEdge = new WeakMap() +// Track window scroll state separately (since WeakMap can't use sentinel reliably) +let windowScrollEdge: ActiveEdge = null +let windowScrollLimit: number | null = null + // Track which group element is currently dragging to clear state on end let currentGroupElement: Element | null = null export function resetAutoScrollState(): void { + // Reset window scroll state + windowScrollEdge = null + windowScrollLimit = null + if (currentGroupElement) { const scrollableAncestor = findScrollableAncestor( currentGroupElement, @@ -56,6 +64,36 @@ function findScrollableAncestor( return null } +function isPageScrollable(axis: "x" | "y"): boolean { + if (typeof document === "undefined") return false + const docEl = document.documentElement + if (axis === "y") { + return docEl.scrollHeight > window.innerHeight + } else { + return docEl.scrollWidth > window.innerWidth + } +} + +function getWindowScrollAmount( + pointerPosition: number, + axis: "x" | "y" +): { amount: number; edge: ActiveEdge } { + const viewportSize = axis === "x" ? window.innerWidth : window.innerHeight + + const distanceFromStart = pointerPosition + const distanceFromEnd = viewportSize - pointerPosition + + if (distanceFromStart < threshold) { + const intensity = 1 - distanceFromStart / threshold + return { amount: -maxSpeed * intensity * intensity, edge: "start" } + } else if (distanceFromEnd < threshold) { + const intensity = 1 - distanceFromEnd / threshold + return { amount: maxSpeed * intensity * intensity, edge: "end" } + } + + return { amount: 0, edge: null } +} + function getScrollAmount( pointerPosition: number, scrollElement: HTMLElement, @@ -92,7 +130,54 @@ export function autoScrollIfNeeded( currentGroupElement = groupElement const scrollableAncestor = findScrollableAncestor(groupElement, axis) - if (!scrollableAncestor) return + + // If no scrollable ancestor, check if page itself is scrollable + if (!scrollableAncestor) { + if (!isPageScrollable(axis)) return + + // Handle page-level scrolling + const { amount: scrollAmount, edge } = getWindowScrollAmount( + pointerPosition, + axis + ) + + // If not in any threshold zone, clear window scroll state + if (edge === null) { + windowScrollEdge = null + windowScrollLimit = null + return + } + + // If not currently scrolling this edge, check velocity to see if we should start + if (windowScrollEdge !== edge) { + const shouldStart = + (edge === "start" && velocity < 0) || + (edge === "end" && velocity > 0) + if (!shouldStart) return + + windowScrollEdge = edge + const docEl = document.documentElement + windowScrollLimit = + axis === "x" + ? docEl.scrollWidth - window.innerWidth + : docEl.scrollHeight - window.innerHeight + } + + // Cap scrolling at initial limit (prevents infinite scroll) + if (scrollAmount > 0 && windowScrollLimit !== null) { + const currentScroll = + axis === "x" ? window.scrollX : window.scrollY + if (currentScroll >= windowScrollLimit) return + } + + // Apply scroll to window + if (axis === "x") { + window.scrollBy(scrollAmount, 0) + } else { + window.scrollBy(0, scrollAmount) + } + return + } const { amount: scrollAmount, edge } = getScrollAmount( pointerPosition, From d41956e1b04710049831c2f42f3b18d7db8f53b8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Jan 2026 10:39:23 +0000 Subject: [PATCH 03/11] Fix autoscroll edge detection when container extends beyond viewport The core fix: clamp the scrollable container's bounds to the viewport when calculating distance from edges. Previously, if a container's bottom was at y=800 but the viewport ended at y=600, dragging to y=580 wouldn't trigger autoscroll because distanceFromEnd was 220px (well above the 50px threshold). Now we use: - start = max(0, rect.top) - end = min(viewportSize, rect.bottom) This ensures autoscroll triggers when near the *visible* edge of the container, regardless of whether the page is scrollable. Also added support for page-level scrolling (using window.scrollBy) when there's no scrollable container but the page itself is scrollable. Fixes #3469 --- .../src/tests/reorder-auto-scroll-page.tsx | 40 ++++++--- .../integration/reorder-auto-scroll.ts | 87 ++++++++++++------- .../components/Reorder/utils/auto-scroll.ts | 8 +- 3 files changed, 89 insertions(+), 46 deletions(-) diff --git a/dev/react/src/tests/reorder-auto-scroll-page.tsx b/dev/react/src/tests/reorder-auto-scroll-page.tsx index 5807375a46..864d0b7425 100644 --- a/dev/react/src/tests/reorder-auto-scroll-page.tsx +++ b/dev/react/src/tests/reorder-auto-scroll-page.tsx @@ -2,7 +2,7 @@ import * as React from "react" import { useState } from "react" import { Reorder, useMotionValue } from "framer-motion" -const initialItems = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] +const initialItems = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] interface ItemProps { item: number @@ -10,7 +10,7 @@ interface ItemProps { const Item = ({ item }: ItemProps) => { const y = useMotionValue(0) - const hue = item * 20 + const hue = item * 30 return ( { } /** - * Test case for auto-scroll when the page itself is scrollable. - * This differs from reorder-auto-scroll.tsx which uses a wrapper div - * with overflow: auto. Here, the document/body is the scroll container. + * Test case for auto-scroll when the scrollable container is inside a scrollable page. + * This tests the bug where autoscroll wouldn't trigger because the container's bounds + * extended beyond the viewport, making the distance calculation incorrect. */ export const App = () => { const [items, setItems] = useState(initialItems) return ( <> -
+ {/* Spacer to make page scrollable and push container partially off-screen */} +
+

+ Spacer - page is scrollable +

+
+
{items.map((item) => ( ))}
+ {/* More spacer to ensure page is scrollable */} +
+

+ Bottom spacer +

+
) @@ -56,11 +76,6 @@ html, body { margin: 0; } -body { - padding: 20px; - box-sizing: border-box; -} - ul, li { list-style: none; @@ -70,8 +85,7 @@ li { ul { position: relative; - width: 300px; - margin: 0 auto; + width: 100%; } li { diff --git a/packages/framer-motion/cypress/integration/reorder-auto-scroll.ts b/packages/framer-motion/cypress/integration/reorder-auto-scroll.ts index 6be867b02f..d0db1f9f1a 100644 --- a/packages/framer-motion/cypress/integration/reorder-auto-scroll.ts +++ b/packages/framer-motion/cypress/integration/reorder-auto-scroll.ts @@ -72,61 +72,86 @@ describe("Reorder auto-scroll", () => { }) }) - describe("with page-level scrolling", () => { - it("Auto-scrolls down when dragging near bottom of viewport", () => { + describe("with scrollable container inside scrollable page", () => { + it("Auto-scrolls container when dragging near visible bottom edge", () => { cy.visit("?test=reorder-auto-scroll-page") .wait(200) - .window() - .then((win) => { - expect(win.scrollY).to.equal(0) + .get("[data-testid='scroll-container']") + .then(($container) => { + expect($container[0].scrollTop).to.equal(0) }) .get("[data-testid='0']") .then(() => { + // Get the visible bottom of the container (clamped to viewport) cy.window().then((win) => { - const viewportHeight = win.innerHeight - const nearBottom = viewportHeight - 20 + cy.get("[data-testid='scroll-container']").then( + ($container) => { + const containerRect = + $container[0].getBoundingClientRect() + // The visible bottom is the min of container bottom and viewport height + const visibleBottom = Math.min( + containerRect.bottom, + win.innerHeight + ) + // Position relative to container top + const dragY = + visibleBottom - containerRect.top - 20 - cy.get("[data-testid='0']") - .trigger("pointerdown", 50, 25) - .wait(50) - .trigger("pointermove", 50, 30, { force: true }) - .wait(50) - .trigger("pointermove", 50, nearBottom, { - force: true, - }) - .wait(300) - .window() - .then((w) => { - expect(w.scrollY).to.be.greaterThan(0) - }) - .get("[data-testid='0']") - .trigger("pointerup", { force: true }) + cy.get("[data-testid='0']") + .trigger("pointerdown", 50, 25) + .wait(50) + .trigger("pointermove", 50, 30, { + force: true, + }) + .wait(50) + .trigger("pointermove", 50, dragY, { + force: true, + }) + .wait(300) + .get("[data-testid='scroll-container']") + .then(($c) => { + expect( + $c[0].scrollTop + ).to.be.greaterThan(0) + }) + .get("[data-testid='0']") + .trigger("pointerup", { force: true }) + } + ) }) }) }) - it("Auto-scrolls up when dragging near top of viewport", () => { + it("Auto-scrolls container when dragging near visible top edge after page scroll", () => { cy.visit("?test=reorder-auto-scroll-page") .wait(200) + // Scroll the page down so the container's top is above viewport .window() .then((win) => { - win.scrollTo(0, 200) + win.scrollTo(0, 250) }) .wait(100) - .window() - .should((win) => { - expect(win.scrollY).to.equal(200) + // Also scroll the container down + .get("[data-testid='scroll-container']") + .then(($container) => { + $container[0].scrollTop = 200 + }) + .wait(100) + .get("[data-testid='scroll-container']") + .should(($container) => { + expect($container[0].scrollTop).to.equal(200) }) .get("[data-testid='8']") .trigger("pointerdown", 50, 25) .wait(50) .trigger("pointermove", 50, 20, { force: true }) .wait(50) - .trigger("pointermove", 50, 10, { force: true }) + // Drag to near top of viewport (which is the visible top of container) + .trigger("pointermove", 50, -100, { force: true }) .wait(300) - .window() - .then((win) => { - expect(win.scrollY).to.be.lessThan(200) + .get("[data-testid='scroll-container']") + .then(($container) => { + expect($container[0].scrollTop).to.be.lessThan(200) }) .get("[data-testid='8']") .trigger("pointerup", { force: true }) diff --git a/packages/framer-motion/src/components/Reorder/utils/auto-scroll.ts b/packages/framer-motion/src/components/Reorder/utils/auto-scroll.ts index 692fa1f2e3..d21c7b372f 100644 --- a/packages/framer-motion/src/components/Reorder/utils/auto-scroll.ts +++ b/packages/framer-motion/src/components/Reorder/utils/auto-scroll.ts @@ -101,8 +101,12 @@ function getScrollAmount( ): { amount: number; edge: ActiveEdge } { const rect = scrollElement.getBoundingClientRect() - const start = axis === "x" ? rect.left : rect.top - const end = axis === "x" ? rect.right : rect.bottom + // Clamp container bounds to the viewport - this ensures autoscroll + // triggers when near the visible edge of the container, even if the + // container extends beyond the viewport (e.g., when page is scrollable) + const viewportSize = axis === "x" ? window.innerWidth : window.innerHeight + const start = Math.max(0, axis === "x" ? rect.left : rect.top) + const end = Math.min(viewportSize, axis === "x" ? rect.right : rect.bottom) const distanceFromStart = pointerPosition - start const distanceFromEnd = end - pointerPosition From 315f9ac541682d882f6909b042d75dbe391db2e3 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Jan 2026 10:45:21 +0000 Subject: [PATCH 04/11] Fix coordinate mismatch in Reorder autoscroll The gesture system uses pageX/pageY coordinates (from extractEventInfo in event-info.ts), but getBoundingClientRect() returns viewport-relative coordinates. When the page is scrolled, these coordinate systems don't match, causing autoscroll to fail. For example, if the page is scrolled 500px: - pointerPosition (pageY) = 800 - rect.top (viewport) = 200 - distanceFromStart = 600 (way above 50px threshold) The fix converts the pointer position to viewport coordinates by subtracting the page scroll offset before comparing with rect bounds. Fixes #3469 --- .../src/tests/reorder-auto-scroll-page.tsx | 17 +-- .../integration/reorder-auto-scroll.ts | 104 ++++++------------ .../components/Reorder/utils/auto-scroll.ts | 101 ++--------------- 3 files changed, 51 insertions(+), 171 deletions(-) diff --git a/dev/react/src/tests/reorder-auto-scroll-page.tsx b/dev/react/src/tests/reorder-auto-scroll-page.tsx index 864d0b7425..0d305300b3 100644 --- a/dev/react/src/tests/reorder-auto-scroll-page.tsx +++ b/dev/react/src/tests/reorder-auto-scroll-page.tsx @@ -27,27 +27,30 @@ const Item = ({ item }: ItemProps) => { /** * Test case for auto-scroll when the scrollable container is inside a scrollable page. - * This tests the bug where autoscroll wouldn't trigger because the container's bounds - * extended beyond the viewport, making the distance calculation incorrect. + * + * This tests the bug where autoscroll wouldn't work because the gesture system + * uses pageX/pageY coordinates but getBoundingClientRect() returns viewport + * coordinates. When the page is scrolled, these coordinate systems don't match. */ export const App = () => { const [items, setItems] = useState(initialItems) return ( <> - {/* Spacer to make page scrollable and push container partially off-screen */} -
+ {/* Spacer to make page scrollable */} +

- Spacer - page is scrollable + Scroll down to see the reorder list. The page is scrollable.

@@ -57,7 +60,7 @@ export const App = () => {
{/* More spacer to ensure page is scrollable */} -
+

Bottom spacer

diff --git a/packages/framer-motion/cypress/integration/reorder-auto-scroll.ts b/packages/framer-motion/cypress/integration/reorder-auto-scroll.ts index d0db1f9f1a..4bd45337d1 100644 --- a/packages/framer-motion/cypress/integration/reorder-auto-scroll.ts +++ b/packages/framer-motion/cypress/integration/reorder-auto-scroll.ts @@ -73,88 +73,48 @@ describe("Reorder auto-scroll", () => { }) describe("with scrollable container inside scrollable page", () => { - it("Auto-scrolls container when dragging near visible bottom edge", () => { + it("Auto-scrolls container after page has been scrolled", () => { cy.visit("?test=reorder-auto-scroll-page") .wait(200) - .get("[data-testid='scroll-container']") - .then(($container) => { - expect($container[0].scrollTop).to.equal(0) - }) - .get("[data-testid='0']") - .then(() => { - // Get the visible bottom of the container (clamped to viewport) - cy.window().then((win) => { - cy.get("[data-testid='scroll-container']").then( - ($container) => { - const containerRect = - $container[0].getBoundingClientRect() - // The visible bottom is the min of container bottom and viewport height - const visibleBottom = Math.min( - containerRect.bottom, - win.innerHeight - ) - // Position relative to container top - const dragY = - visibleBottom - containerRect.top - 20 - - cy.get("[data-testid='0']") - .trigger("pointerdown", 50, 25) - .wait(50) - .trigger("pointermove", 50, 30, { - force: true, - }) - .wait(50) - .trigger("pointermove", 50, dragY, { - force: true, - }) - .wait(300) - .get("[data-testid='scroll-container']") - .then(($c) => { - expect( - $c[0].scrollTop - ).to.be.greaterThan(0) - }) - .get("[data-testid='0']") - .trigger("pointerup", { force: true }) - } - ) - }) - }) - }) - - it("Auto-scrolls container when dragging near visible top edge after page scroll", () => { - cy.visit("?test=reorder-auto-scroll-page") - .wait(200) - // Scroll the page down so the container's top is above viewport + // Scroll the page down so the container is in view .window() .then((win) => { - win.scrollTo(0, 250) + win.scrollTo(0, 200) }) .wait(100) - // Also scroll the container down .get("[data-testid='scroll-container']") .then(($container) => { - $container[0].scrollTop = 200 - }) - .wait(100) - .get("[data-testid='scroll-container']") - .should(($container) => { - expect($container[0].scrollTop).to.equal(200) + expect($container[0].scrollTop).to.equal(0) }) - .get("[data-testid='8']") - .trigger("pointerdown", 50, 25) - .wait(50) - .trigger("pointermove", 50, 20, { force: true }) - .wait(50) - // Drag to near top of viewport (which is the visible top of container) - .trigger("pointermove", 50, -100, { force: true }) - .wait(300) - .get("[data-testid='scroll-container']") - .then(($container) => { - expect($container[0].scrollTop).to.be.lessThan(200) + .get("[data-testid='0']") + .then(() => { + cy.get("[data-testid='scroll-container']").then( + ($container) => { + const containerRect = + $container[0].getBoundingClientRect() + const nearBottom = + containerRect.bottom - containerRect.top - 20 + + cy.get("[data-testid='0']") + .trigger("pointerdown", 50, 25) + .wait(50) + .trigger("pointermove", 50, 30, { force: true }) + .wait(50) + .trigger("pointermove", 50, nearBottom, { + force: true, + }) + .wait(300) + .get("[data-testid='scroll-container']") + .then(($c) => { + expect( + $c[0].scrollTop + ).to.be.greaterThan(0) + }) + .get("[data-testid='0']") + .trigger("pointerup", { force: true }) + } + ) }) - .get("[data-testid='8']") - .trigger("pointerup", { force: true }) }) }) }) diff --git a/packages/framer-motion/src/components/Reorder/utils/auto-scroll.ts b/packages/framer-motion/src/components/Reorder/utils/auto-scroll.ts index d21c7b372f..154ddba85c 100644 --- a/packages/framer-motion/src/components/Reorder/utils/auto-scroll.ts +++ b/packages/framer-motion/src/components/Reorder/utils/auto-scroll.ts @@ -10,18 +10,10 @@ const initialScrollLimits = new WeakMap() type ActiveEdge = "start" | "end" | null const activeScrollEdge = new WeakMap() -// Track window scroll state separately (since WeakMap can't use sentinel reliably) -let windowScrollEdge: ActiveEdge = null -let windowScrollLimit: number | null = null - // Track which group element is currently dragging to clear state on end let currentGroupElement: Element | null = null export function resetAutoScrollState(): void { - // Reset window scroll state - windowScrollEdge = null - windowScrollLimit = null - if (currentGroupElement) { const scrollableAncestor = findScrollableAncestor( currentGroupElement, @@ -64,36 +56,6 @@ function findScrollableAncestor( return null } -function isPageScrollable(axis: "x" | "y"): boolean { - if (typeof document === "undefined") return false - const docEl = document.documentElement - if (axis === "y") { - return docEl.scrollHeight > window.innerHeight - } else { - return docEl.scrollWidth > window.innerWidth - } -} - -function getWindowScrollAmount( - pointerPosition: number, - axis: "x" | "y" -): { amount: number; edge: ActiveEdge } { - const viewportSize = axis === "x" ? window.innerWidth : window.innerHeight - - const distanceFromStart = pointerPosition - const distanceFromEnd = viewportSize - pointerPosition - - if (distanceFromStart < threshold) { - const intensity = 1 - distanceFromStart / threshold - return { amount: -maxSpeed * intensity * intensity, edge: "start" } - } else if (distanceFromEnd < threshold) { - const intensity = 1 - distanceFromEnd / threshold - return { amount: maxSpeed * intensity * intensity, edge: "end" } - } - - return { amount: 0, edge: null } -} - function getScrollAmount( pointerPosition: number, scrollElement: HTMLElement, @@ -101,12 +63,8 @@ function getScrollAmount( ): { amount: number; edge: ActiveEdge } { const rect = scrollElement.getBoundingClientRect() - // Clamp container bounds to the viewport - this ensures autoscroll - // triggers when near the visible edge of the container, even if the - // container extends beyond the viewport (e.g., when page is scrollable) - const viewportSize = axis === "x" ? window.innerWidth : window.innerHeight - const start = Math.max(0, axis === "x" ? rect.left : rect.top) - const end = Math.min(viewportSize, axis === "x" ? rect.right : rect.bottom) + const start = axis === "x" ? rect.left : rect.top + const end = axis === "x" ? rect.right : rect.bottom const distanceFromStart = pointerPosition - start const distanceFromEnd = end - pointerPosition @@ -134,57 +92,16 @@ export function autoScrollIfNeeded( currentGroupElement = groupElement const scrollableAncestor = findScrollableAncestor(groupElement, axis) + if (!scrollableAncestor) return - // If no scrollable ancestor, check if page itself is scrollable - if (!scrollableAncestor) { - if (!isPageScrollable(axis)) return - - // Handle page-level scrolling - const { amount: scrollAmount, edge } = getWindowScrollAmount( - pointerPosition, - axis - ) - - // If not in any threshold zone, clear window scroll state - if (edge === null) { - windowScrollEdge = null - windowScrollLimit = null - return - } - - // If not currently scrolling this edge, check velocity to see if we should start - if (windowScrollEdge !== edge) { - const shouldStart = - (edge === "start" && velocity < 0) || - (edge === "end" && velocity > 0) - if (!shouldStart) return - - windowScrollEdge = edge - const docEl = document.documentElement - windowScrollLimit = - axis === "x" - ? docEl.scrollWidth - window.innerWidth - : docEl.scrollHeight - window.innerHeight - } - - // Cap scrolling at initial limit (prevents infinite scroll) - if (scrollAmount > 0 && windowScrollLimit !== null) { - const currentScroll = - axis === "x" ? window.scrollX : window.scrollY - if (currentScroll >= windowScrollLimit) return - } - - // Apply scroll to window - if (axis === "x") { - window.scrollBy(scrollAmount, 0) - } else { - window.scrollBy(0, scrollAmount) - } - return - } + // Convert pointer position from page coordinates to viewport coordinates. + // The gesture system uses pageX/pageY but getBoundingClientRect() returns + // viewport-relative coordinates, so we need to account for page scroll. + const viewportPointerPosition = + pointerPosition - (axis === "x" ? window.scrollX : window.scrollY) const { amount: scrollAmount, edge } = getScrollAmount( - pointerPosition, + viewportPointerPosition, scrollableAncestor, axis ) From 0987b285ba60d12fc9e2a88a787132c925505ecc Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Mon, 19 Jan 2026 15:45:34 +0100 Subject: [PATCH 05/11] Updating changelog --- .../animate-layout/app-store-a-b-a.html | 328 ++++++++++++++++++ 1 file changed, 328 insertions(+) create mode 100644 dev/html/public/animate-layout/app-store-a-b-a.html diff --git a/dev/html/public/animate-layout/app-store-a-b-a.html b/dev/html/public/animate-layout/app-store-a-b-a.html new file mode 100644 index 0000000000..3975eb6ba5 --- /dev/null +++ b/dev/html/public/animate-layout/app-store-a-b-a.html @@ -0,0 +1,328 @@ + + + + + + +

App Store Card Test (A → B → A)

+

Click a card to open, click overlay or press Escape to close. Try opening the same card multiple times.

+ +
    +
  • +
    + +
    + Travel +

    Mountain Adventures

    +
    +

    + Discover breathtaking mountain landscapes and plan your next hiking adventure. +

    +
    +
  • + +
  • +
    + +
    + Ocean +

    Coastal Escapes

    +
    +

    + From serene beaches to dramatic cliffs, explore stunning coastlines. +

    +
    +
  • + +
  • +
    + +
    + Nature +

    Forest Retreats

    +
    +

    + Immerse yourself in ancient forests and discover hidden trails. +

    +
    +
  • +
+ +
+ Animation count: 0 +
+ + + + + + From 2b9f97a5de36493728ec7df6aa7534dd1ac51645 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Tue, 20 Jan 2026 09:29:30 +0100 Subject: [PATCH 06/11] Removing old demo --- .../shared-element-app-store.html | 485 ------------------ 1 file changed, 485 deletions(-) delete mode 100644 dev/html/public/animate-layout/shared-element-app-store.html diff --git a/dev/html/public/animate-layout/shared-element-app-store.html b/dev/html/public/animate-layout/shared-element-app-store.html deleted file mode 100644 index d27d7d1d16..0000000000 --- a/dev/html/public/animate-layout/shared-element-app-store.html +++ /dev/null @@ -1,485 +0,0 @@ - - - - - - -
-
    - -
  • -
    -
    -
    -
    -
    -

    Travel

    -

    Card A Title

    -
    -
    -
  • - - -
  • -
    -
    -
    -
    -
    -

    How to

    -

    Card C Title

    -
    -
    -
  • - - -
  • -
    -
    -
    -
    -
    -

    Steps

    -

    Card D Title

    -
    -
    -
  • - - -
  • -
    -
    -
    -
    -
    -

    Hats

    -

    Card B Title

    -
    -
    -
  • -
-
- - - - - - From 6ccb2ed91fcadf77cbc780b0bba0974198c4c3d8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 19 Jan 2026 10:38:08 +0000 Subject: [PATCH 07/11] Fix animateSequence to gracefully handle null/undefined elements Previously, passing null elements to animateSequence (e.g., from querySelector returning null) would throw an error. This was a regression from Motion 10's permissive behavior. Changes: - Update resolveElements in motion-dom to return empty array for null/undefined input and filter null elements from arrays - Update resolveSubjects in framer-motion to handle null/undefined subjects gracefully - Add early return for null/undefined in animateSubject and animateElements to gracefully skip (distinct from throwing for selectors that match no elements) - Add tests for null element handling in sequences Fixes #3390 --- .../src/animation/animate/resolve-subjects.ts | 16 ++++++- .../src/animation/animate/subject.ts | 11 +++-- .../animators/waapi/animate-elements.ts | 5 +++ .../sequence/__tests__/index.test.ts | 44 +++++++++++++++++++ .../motion-dom/src/utils/resolve-elements.ts | 10 ++++- 5 files changed, 77 insertions(+), 9 deletions(-) diff --git a/packages/framer-motion/src/animation/animate/resolve-subjects.ts b/packages/framer-motion/src/animation/animate/resolve-subjects.ts index 4996c46d36..395144e48a 100644 --- a/packages/framer-motion/src/animation/animate/resolve-subjects.ts +++ b/packages/framer-motion/src/animation/animate/resolve-subjects.ts @@ -8,17 +8,29 @@ import { ObjectTarget } from "../sequence/types" import { isDOMKeyframes } from "../utils/is-dom-keyframes" export function resolveSubjects( - subject: string | Element | Element[] | NodeListOf | O | O[], + subject: + | string + | Element + | Element[] + | NodeListOf + | O + | O[] + | null + | undefined, keyframes: DOMKeyframesDefinition | ObjectTarget, scope?: AnimationScope, selectorCache?: SelectorCache ) { + if (subject == null) { + return [] + } + if (typeof subject === "string" && isDOMKeyframes(keyframes)) { return resolveElements(subject, scope, selectorCache) } else if (subject instanceof NodeList) { return Array.from(subject) } else if (Array.isArray(subject)) { - return subject + return subject.filter((s) => s != null) } else { return [subject] } diff --git a/packages/framer-motion/src/animation/animate/subject.ts b/packages/framer-motion/src/animation/animate/subject.ts index fc09b0b7d2..67f8cc6684 100644 --- a/packages/framer-motion/src/animation/animate/subject.ts +++ b/packages/framer-motion/src/animation/animate/subject.ts @@ -107,6 +107,11 @@ export function animateSubject( ) ) } else { + // Gracefully handle null/undefined subjects (e.g., from querySelector returning null) + if (subject == null) { + return animations + } + const subjects = resolveSubjects( subject, keyframes as DOMKeyframesDefinition, @@ -124,12 +129,6 @@ export function animateSubject( for (let i = 0; i < numSubjects; i++) { const thisSubject = subjects[i] - invariant( - thisSubject !== null, - "You're trying to perform an animation on null. Ensure that selectors are correctly finding elements and refs are correctly hydrated.", - "animate-null" - ) - const createVisualElement = thisSubject instanceof Element ? createDOMVisualElement diff --git a/packages/framer-motion/src/animation/animators/waapi/animate-elements.ts b/packages/framer-motion/src/animation/animators/waapi/animate-elements.ts index 65e5604070..7fb0945610 100644 --- a/packages/framer-motion/src/animation/animators/waapi/animate-elements.ts +++ b/packages/framer-motion/src/animation/animators/waapi/animate-elements.ts @@ -33,6 +33,11 @@ export function animateElements( options?: DynamicAnimationOptions, scope?: AnimationScope ) { + // Gracefully handle null/undefined elements (e.g., from querySelector returning null) + if (elementOrSelector == null) { + return [] + } + const elements = resolveElements(elementOrSelector, scope) as Array< HTMLElement | SVGElement > diff --git a/packages/framer-motion/src/animation/sequence/__tests__/index.test.ts b/packages/framer-motion/src/animation/sequence/__tests__/index.test.ts index c57c47c72d..1b072c7672 100644 --- a/packages/framer-motion/src/animation/sequence/__tests__/index.test.ts +++ b/packages/framer-motion/src/animation/sequence/__tests__/index.test.ts @@ -768,4 +768,48 @@ describe("createAnimationsFromSequence", () => { expect(duration).toEqual(4) expect(times).toEqual([0, 0.25, 0.25, 0.5, 0.5, 0.75, 0.75, 1]) }) + + test("It skips null elements in sequence", () => { + const animations = createAnimationsFromSequence( + [ + [a, { opacity: 1 }, { duration: 1 }], + [null as unknown as Element, { opacity: 0.5 }, { duration: 1 }], + [b, { opacity: 0 }, { duration: 1 }], + ], + undefined, + undefined, + { spring } + ) + + // Should only have animations for a and b, not the null element + expect(animations.size).toBe(2) + expect(animations.has(a)).toBe(true) + expect(animations.has(b)).toBe(true) + }) + + test("It filters null elements from array of targets", () => { + const animations = createAnimationsFromSequence( + [[[a, null as unknown as Element, b], { x: 100 }, { duration: 1 }]], + undefined, + undefined, + { spring } + ) + + // Should only have animations for a and b, not the null element + expect(animations.size).toBe(2) + expect(animations.has(a)).toBe(true) + expect(animations.has(b)).toBe(true) + }) + + test("It handles sequence with only null element gracefully", () => { + const animations = createAnimationsFromSequence( + [[null as unknown as Element, { opacity: 1 }, { duration: 1 }]], + undefined, + undefined, + { spring } + ) + + // Should return empty map when no valid elements + expect(animations.size).toBe(0) + }) }) diff --git a/packages/motion-dom/src/utils/resolve-elements.ts b/packages/motion-dom/src/utils/resolve-elements.ts index 9de64eaf79..00455617ec 100644 --- a/packages/motion-dom/src/utils/resolve-elements.ts +++ b/packages/motion-dom/src/utils/resolve-elements.ts @@ -3,6 +3,8 @@ export type ElementOrSelector = | Element[] | NodeListOf | string + | null + | undefined export interface WithQuerySelectorAll { querySelectorAll: Element["querySelectorAll"] @@ -22,6 +24,10 @@ export function resolveElements( scope?: AnimationScope, selectorCache?: SelectorCache ): Element[] { + if (elementOrSelector == null) { + return [] + } + if (elementOrSelector instanceof EventTarget) { return [elementOrSelector] } else if (typeof elementOrSelector === "string") { @@ -38,5 +44,7 @@ export function resolveElements( return elements ? Array.from(elements) : [] } - return Array.from(elementOrSelector) + return Array.from(elementOrSelector).filter( + (element): element is Element => element != null + ) } From 72fff16563d62f27b71e055ea9a456ce03938ad5 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Tue, 20 Jan 2026 10:08:29 +0100 Subject: [PATCH 08/11] Updating changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5dbe5b1763..ea44b0432c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ Motion adheres to [Semantic Versioning](http://semver.org/). Undocumented APIs should be considered internal and may change without warning. +## [12.27.2] 2026-01-20 + +### Fixed + +- Adding sourcemaps to `motion-dom` and `motion-utils`. + ## [12.27.1] 2026-01-19 ### Fixed From 4ed7545fc9c35324f17e4d092bc2c9cdb7d33140 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Tue, 20 Jan 2026 10:09:41 +0100 Subject: [PATCH 09/11] Updating changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea44b0432c..358118c178 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Undocumented APIs should be considered internal and may change without warning. ### Fixed - Adding sourcemaps to `motion-dom` and `motion-utils`. +- Fix `Reorder` autoscroll within scrollable pages. ## [12.27.1] 2026-01-19 From 6f5e0ba45ee5cb9b1d4221d3866a4613f9d80d9e Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Tue, 20 Jan 2026 10:17:54 +0100 Subject: [PATCH 10/11] Updating changelog --- CHANGELOG.md | 1 + .../src/gestures/drag/__tests__/index.test.tsx | 9 ++------- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 358118c178..3aeb8f2dcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Undocumented APIs should be considered internal and may change without warning. - Adding sourcemaps to `motion-dom` and `motion-utils`. - Fix `Reorder` autoscroll within scrollable pages. +- Gracefully handle missing elements in animation sequences. ## [12.27.1] 2026-01-19 diff --git a/packages/framer-motion/src/gestures/drag/__tests__/index.test.tsx b/packages/framer-motion/src/gestures/drag/__tests__/index.test.tsx index 4ebddda0e7..cbae823c7d 100644 --- a/packages/framer-motion/src/gestures/drag/__tests__/index.test.tsx +++ b/packages/framer-motion/src/gestures/drag/__tests__/index.test.tsx @@ -540,13 +540,8 @@ describe("dragging", () => { await pointer.to(20, 0) pointer.end() - expect({ - parentX: parentX.get(), - childX: childX.get(), - }).toEqual({ - parentX: 20, - childX: 20, - }) + expect(parentX.get()).toBeCloseTo(20) + expect(childX.get()).toBeCloseTo(20) }) test("whileDrag applies animation state", async () => { From a93d4ff47460ef01b1a42580700c770d5f28b040 Mon Sep 17 00:00:00 2001 From: Matt Perry Date: Tue, 20 Jan 2026 10:36:35 +0100 Subject: [PATCH 11/11] v12.27.2 --- dev/html/package.json | 8 ++++---- dev/next/package.json | 4 ++-- dev/react-19/package.json | 4 ++-- dev/react/package.json | 4 ++-- lerna.json | 2 +- packages/framer-motion/package.json | 6 +++--- packages/motion-dom/package.json | 4 ++-- packages/motion-utils/package.json | 2 +- packages/motion/package.json | 4 ++-- yarn.lock | 28 ++++++++++++++-------------- 10 files changed, 33 insertions(+), 33 deletions(-) diff --git a/dev/html/package.json b/dev/html/package.json index 7c9725e6a0..f0e64c9cf8 100644 --- a/dev/html/package.json +++ b/dev/html/package.json @@ -1,7 +1,7 @@ { "name": "html-env", "private": true, - "version": "12.27.1", + "version": "12.27.2", "type": "module", "scripts": { "dev": "vite", @@ -10,9 +10,9 @@ "preview": "vite preview" }, "dependencies": { - "framer-motion": "^12.27.1", - "motion": "^12.27.1", - "motion-dom": "^12.27.1", + "framer-motion": "^12.27.2", + "motion": "^12.27.2", + "motion-dom": "^12.27.2", "react": "^18.3.1", "react-dom": "^18.3.1" }, diff --git a/dev/next/package.json b/dev/next/package.json index 1ceb7cd71a..0b1543592d 100644 --- a/dev/next/package.json +++ b/dev/next/package.json @@ -1,7 +1,7 @@ { "name": "next-env", "private": true, - "version": "12.27.1", + "version": "12.27.2", "type": "module", "scripts": { "dev": "next dev", @@ -10,7 +10,7 @@ "build": "next build" }, "dependencies": { - "motion": "^12.27.1", + "motion": "^12.27.2", "next": "15.4.10", "react": "19.0.0", "react-dom": "19.0.0" diff --git a/dev/react-19/package.json b/dev/react-19/package.json index 436b5b754c..2a63004b72 100644 --- a/dev/react-19/package.json +++ b/dev/react-19/package.json @@ -1,7 +1,7 @@ { "name": "react-19-env", "private": true, - "version": "12.27.1", + "version": "12.27.2", "type": "module", "scripts": { "dev": "vite", @@ -11,7 +11,7 @@ "preview": "vite preview" }, "dependencies": { - "motion": "^12.27.1", + "motion": "^12.27.2", "react": "^19.0.0", "react-dom": "^19.0.0" }, diff --git a/dev/react/package.json b/dev/react/package.json index 0092a87e2f..5b09dddd50 100644 --- a/dev/react/package.json +++ b/dev/react/package.json @@ -1,7 +1,7 @@ { "name": "react-env", "private": true, - "version": "12.27.1", + "version": "12.27.2", "type": "module", "scripts": { "dev": "yarn vite", @@ -11,7 +11,7 @@ "preview": "yarn vite preview" }, "dependencies": { - "framer-motion": "^12.27.1", + "framer-motion": "^12.27.2", "react": "^18.3.1", "react-dom": "^18.3.1" }, diff --git a/lerna.json b/lerna.json index e3d94d22af..fa7cad9a23 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "12.27.1", + "version": "12.27.2", "packages": [ "packages/*", "dev/*" diff --git a/packages/framer-motion/package.json b/packages/framer-motion/package.json index 82abab8a37..09276d376f 100644 --- a/packages/framer-motion/package.json +++ b/packages/framer-motion/package.json @@ -1,6 +1,6 @@ { "name": "framer-motion", - "version": "12.27.1", + "version": "12.27.2", "description": "A simple and powerful JavaScript animation library", "main": "dist/cjs/index.js", "module": "dist/es/index.mjs", @@ -88,8 +88,8 @@ "measure": "rollup -c ./rollup.size.config.mjs" }, "dependencies": { - "motion-dom": "^12.27.1", - "motion-utils": "^12.24.10", + "motion-dom": "^12.27.2", + "motion-utils": "^12.27.2", "tslib": "^2.4.0" }, "devDependencies": { diff --git a/packages/motion-dom/package.json b/packages/motion-dom/package.json index ffb55f768e..fd528dd3eb 100644 --- a/packages/motion-dom/package.json +++ b/packages/motion-dom/package.json @@ -1,6 +1,6 @@ { "name": "motion-dom", - "version": "12.27.1", + "version": "12.27.2", "author": "Matt Perry", "license": "MIT", "repository": "https://github.com/motiondivision/motion", @@ -17,7 +17,7 @@ } }, "dependencies": { - "motion-utils": "^12.24.10" + "motion-utils": "^12.27.2" }, "scripts": { "clean": "rm -rf types dist lib", diff --git a/packages/motion-utils/package.json b/packages/motion-utils/package.json index 170df3fa17..e58196a0e9 100644 --- a/packages/motion-utils/package.json +++ b/packages/motion-utils/package.json @@ -1,6 +1,6 @@ { "name": "motion-utils", - "version": "12.24.10", + "version": "12.27.2", "author": "Matt Perry", "license": "MIT", "repository": "https://github.com/motiondivision/motion", diff --git a/packages/motion/package.json b/packages/motion/package.json index f2ffa9d46c..f1f8de1aa8 100644 --- a/packages/motion/package.json +++ b/packages/motion/package.json @@ -1,6 +1,6 @@ { "name": "motion", - "version": "12.27.1", + "version": "12.27.2", "description": "An animation library for JavaScript and React.", "main": "dist/cjs/index.js", "module": "dist/es/index.mjs", @@ -76,7 +76,7 @@ "postpublish": "git push --tags" }, "dependencies": { - "framer-motion": "^12.27.1", + "framer-motion": "^12.27.2", "tslib": "^2.4.0" }, "peerDependencies": { diff --git a/yarn.lock b/yarn.lock index 4a52809dfe..5d03ae3257 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7420,15 +7420,15 @@ __metadata: languageName: node linkType: hard -"framer-motion@^12.27.1, framer-motion@workspace:packages/framer-motion": +"framer-motion@^12.27.2, framer-motion@workspace:packages/framer-motion": version: 0.0.0-use.local resolution: "framer-motion@workspace:packages/framer-motion" dependencies: "@radix-ui/react-dialog": ^1.1.15 "@thednp/dommatrix": ^2.0.11 "@types/three": 0.137.0 - motion-dom: ^12.27.1 - motion-utils: ^12.24.10 + motion-dom: ^12.27.2 + motion-utils: ^12.27.2 three: 0.137.0 tslib: ^2.4.0 peerDependencies: @@ -8192,9 +8192,9 @@ __metadata: version: 0.0.0-use.local resolution: "html-env@workspace:dev/html" dependencies: - framer-motion: ^12.27.1 - motion: ^12.27.1 - motion-dom: ^12.27.1 + framer-motion: ^12.27.2 + motion: ^12.27.2 + motion-dom: ^12.27.2 react: ^18.3.1 react-dom: ^18.3.1 vite: ^5.2.0 @@ -10936,11 +10936,11 @@ __metadata: languageName: node linkType: hard -"motion-dom@^12.27.1, motion-dom@workspace:packages/motion-dom": +"motion-dom@^12.27.2, motion-dom@workspace:packages/motion-dom": version: 0.0.0-use.local resolution: "motion-dom@workspace:packages/motion-dom" dependencies: - motion-utils: ^12.24.10 + motion-utils: ^12.27.2 languageName: unknown linkType: soft @@ -11007,17 +11007,17 @@ __metadata: languageName: unknown linkType: soft -"motion-utils@^12.24.10, motion-utils@workspace:packages/motion-utils": +"motion-utils@^12.27.2, motion-utils@workspace:packages/motion-utils": version: 0.0.0-use.local resolution: "motion-utils@workspace:packages/motion-utils" languageName: unknown linkType: soft -"motion@^12.27.1, motion@workspace:packages/motion": +"motion@^12.27.2, motion@workspace:packages/motion": version: 0.0.0-use.local resolution: "motion@workspace:packages/motion" dependencies: - framer-motion: ^12.27.1 + framer-motion: ^12.27.2 tslib: ^2.4.0 peerDependencies: "@emotion/is-prop-valid": "*" @@ -11134,7 +11134,7 @@ __metadata: version: 0.0.0-use.local resolution: "next-env@workspace:dev/next" dependencies: - motion: ^12.27.1 + motion: ^12.27.2 next: 15.4.10 react: 19.0.0 react-dom: 19.0.0 @@ -12599,7 +12599,7 @@ __metadata: "@typescript-eslint/parser": ^7.2.0 "@vitejs/plugin-react-swc": ^3.5.0 eslint-plugin-react-refresh: ^0.4.6 - motion: ^12.27.1 + motion: ^12.27.2 react: ^19.0.0 react-dom: ^19.0.0 vite: ^5.2.0 @@ -12683,7 +12683,7 @@ __metadata: "@typescript-eslint/parser": ^7.2.0 "@vitejs/plugin-react-swc": ^3.5.0 eslint-plugin-react-refresh: ^0.4.6 - framer-motion: ^12.27.1 + framer-motion: ^12.27.2 react: ^18.3.1 react-dom: ^18.3.1 vite: ^5.2.0