diff --git a/CHANGELOG.md b/CHANGELOG.md
index 561c903e8d..20d774cce7 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.34.3] 2026-02-20
+
+### Fixed
+
+- Ensure `velocity` is never transferred to a time-derived spring.
+
## [12.34.2] 2026-02-18
### Fixed
diff --git a/dev/html/package.json b/dev/html/package.json
index e3886a6c42..df2d80637a 100644
--- a/dev/html/package.json
+++ b/dev/html/package.json
@@ -1,7 +1,7 @@
{
"name": "html-env",
"private": true,
- "version": "12.34.2",
+ "version": "12.34.3",
"type": "module",
"scripts": {
"dev": "vite",
@@ -10,9 +10,9 @@
"preview": "vite preview"
},
"dependencies": {
- "framer-motion": "^12.34.2",
- "motion": "^12.34.2",
- "motion-dom": "^12.34.2",
+ "framer-motion": "^12.34.3",
+ "motion": "^12.34.3",
+ "motion-dom": "^12.34.3",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
diff --git a/dev/next/package.json b/dev/next/package.json
index be434805a6..b18a0322b4 100644
--- a/dev/next/package.json
+++ b/dev/next/package.json
@@ -1,7 +1,7 @@
{
"name": "next-env",
"private": true,
- "version": "12.34.2",
+ "version": "12.34.3",
"type": "module",
"scripts": {
"dev": "next dev",
@@ -10,7 +10,7 @@
"build": "next build"
},
"dependencies": {
- "motion": "^12.34.2",
+ "motion": "^12.34.3",
"next": "15.5.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 783ae187ce..cc925c6d4e 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.34.2",
+ "version": "12.34.3",
"type": "module",
"scripts": {
"dev": "vite",
@@ -11,7 +11,7 @@
"preview": "vite preview"
},
"dependencies": {
- "motion": "^12.34.2",
+ "motion": "^12.34.3",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
diff --git a/dev/react/package.json b/dev/react/package.json
index 40a9a13c31..90e6316b5c 100644
--- a/dev/react/package.json
+++ b/dev/react/package.json
@@ -1,7 +1,7 @@
{
"name": "react-env",
"private": true,
- "version": "12.34.2",
+ "version": "12.34.3",
"type": "module",
"scripts": {
"dev": "yarn vite",
@@ -11,7 +11,7 @@
"preview": "yarn vite preview"
},
"dependencies": {
- "framer-motion": "^12.34.2",
+ "framer-motion": "^12.34.3",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
diff --git a/dev/react/src/tests/layout-appear-spring-bounce.tsx b/dev/react/src/tests/layout-appear-spring-bounce.tsx
new file mode 100644
index 0000000000..93e7e2052a
--- /dev/null
+++ b/dev/react/src/tests/layout-appear-spring-bounce.tsx
@@ -0,0 +1,109 @@
+import { animate, motion, useMotionValue } from "framer-motion"
+import { useEffect, useRef } from "react"
+
+/**
+ * Reproduces the bug where a time-defined spring receives velocity from
+ * an interrupted animation, causing wild oscillation.
+ *
+ * The Framer scenario:
+ * - Appear effect animates opacity 0.001 → 1 with time-defined spring
+ * - On hover, opacity → 0.49 with same spring
+ * - WAAPI appear animation sets velocity on motionValue when stopped
+ * - Hover animation reads velocity, passes it to findSpring()
+ * - findSpring() computes wrong spring parameters → wild oscillation
+ *
+ * This test uses external motionValues (no WAAPI owner → JS animation)
+ * with explicit velocity injection to simulate the WAAPI handoff.
+ */
+
+const springTransition = {
+ type: "spring" as const,
+ duration: 0.4,
+ bounce: 0.2,
+}
+
+export const App = () =>
+
+function ExternalMotionValueMode() {
+ // Start at 0.5 (simulating appear animation mid-flight)
+ const opacity = useMotionValue(0.5)
+ const scale = useMotionValue(1)
+ const trackerRef = useRef(null)
+
+ useEffect(() => {
+ // Simulate WAAPI handoff: inject velocity as if appear animation
+ // was stopped mid-flight (NativeAnimationExtended.updateMotionValue
+ // calls setWithVelocity on the motionValue)
+ const sampleDelta = 10 // ms, same as NativeAnimationExtended
+ opacity.setWithVelocity(
+ 0.45, // prev sample (velocity ~5/s upward)
+ 0.5, // current
+ sampleDelta
+ )
+
+ // Start hover animation — reads velocity from motionValue
+ animate(opacity, 0.49, springTransition)
+ animate(scale, 1.1, springTransition)
+
+ // Track min/max values during hover animation
+ let minOpacity = 0.5
+ let maxOpacity = 0.5
+ let minScale = 1
+ let maxScale = 1
+
+ const unsubOpacity = opacity.on("change", (v) => {
+ if (v < minOpacity) minOpacity = v
+ if (v > maxOpacity) maxOpacity = v
+ if (trackerRef.current) {
+ trackerRef.current.dataset.minOpacity = minOpacity.toFixed(4)
+ trackerRef.current.dataset.maxOpacity = maxOpacity.toFixed(4)
+ }
+ })
+
+ const unsubScale = scale.on("change", (v) => {
+ if (v < minScale) minScale = v
+ if (v > maxScale) maxScale = v
+ if (trackerRef.current) {
+ trackerRef.current.dataset.minScale = minScale.toFixed(4)
+ trackerRef.current.dataset.maxScale = maxScale.toFixed(4)
+ }
+ })
+
+ return () => {
+ unsubOpacity()
+ unsubScale()
+ }
+ }, [])
+
+ return (
+ <>
+
+
+
+
+ >
+ )
+}
diff --git a/lerna.json b/lerna.json
index cb3ba270a4..01ade49e7f 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,5 +1,5 @@
{
- "version": "12.34.2",
+ "version": "12.34.3",
"packages": [
"packages/*",
"dev/*"
diff --git a/packages/framer-motion/cypress/integration/layout-appear-spring-bounce.ts b/packages/framer-motion/cypress/integration/layout-appear-spring-bounce.ts
new file mode 100644
index 0000000000..3d7b849760
--- /dev/null
+++ b/packages/framer-motion/cypress/integration/layout-appear-spring-bounce.ts
@@ -0,0 +1,30 @@
+describe("Time-defined spring with inherited velocity", () => {
+ it("Doesn't wildly oscillate when velocity is inherited from interrupted animation", () => {
+ /**
+ * Reproduces the Framer bug:
+ * 1. Appear animation sets velocity on motionValue when stopped
+ * 2. Hover animation reads velocity and passes to time-defined spring
+ * 3. findSpring() computes wrong parameters → wild oscillation
+ *
+ * Opacity starts at 0.5 with +5/s velocity (simulating interrupted
+ * appear). Hover targets 0.49. Without fix, opacity shoots up to
+ * ~0.58+ before settling. With fix, it stays near 0.5.
+ */
+ cy.visit("?test=layout-appear-spring-bounce")
+ .wait(1500)
+ .get("#tracker")
+ .should(([$tracker]: any) => {
+ const maxOpacity = Number($tracker.dataset.maxOpacity)
+
+ // Opacity starts at 0.5, targets 0.49 (tiny delta of 0.01)
+ // A well-behaved spring should barely overshoot above 0.5
+ // Bug: velocity causes overshoot to ~0.58+
+ // Fixed: maxOpacity stays near 0.5
+ expect(maxOpacity).to.be.lessThan(
+ 0.55,
+ `Opacity overshot to ${maxOpacity} (start: 0.5, target: 0.49). ` +
+ `Time-defined spring should ignore inherited velocity.`
+ )
+ })
+ })
+})
diff --git a/packages/framer-motion/package.json b/packages/framer-motion/package.json
index 6b6a95a904..2d774d979a 100644
--- a/packages/framer-motion/package.json
+++ b/packages/framer-motion/package.json
@@ -1,6 +1,6 @@
{
"name": "framer-motion",
- "version": "12.34.2",
+ "version": "12.34.3",
"description": "A simple and powerful JavaScript animation library",
"main": "dist/cjs/index.js",
"module": "dist/es/index.mjs",
@@ -88,7 +88,7 @@
"measure": "rollup -c ./rollup.size.config.mjs"
},
"dependencies": {
- "motion-dom": "^12.34.2",
+ "motion-dom": "^12.34.3",
"motion-utils": "^12.29.2",
"tslib": "^2.4.0"
},
diff --git a/packages/framer-motion/src/gestures/pan/PanSession.ts b/packages/framer-motion/src/gestures/pan/PanSession.ts
index 006b7c065f..afac94615e 100644
--- a/packages/framer-motion/src/gestures/pan/PanSession.ts
+++ b/packages/framer-motion/src/gestures/pan/PanSession.ts
@@ -194,13 +194,10 @@ export class PanSession {
// Capture listener catches element scroll events as they bubble
window.addEventListener("scroll", this.onElementScroll, {
capture: true,
- passive: true,
})
// Direct window scroll listener (window scroll doesn't bubble)
- window.addEventListener("scroll", this.onWindowScroll, {
- passive: true,
- })
+ window.addEventListener("scroll", this.onWindowScroll)
this.removeScrollListeners = () => {
window.removeEventListener("scroll", this.onElementScroll, {
diff --git a/packages/framer-motion/src/render/dom/scroll/track.ts b/packages/framer-motion/src/render/dom/scroll/track.ts
index 4326a6b121..e6cbf382c5 100644
--- a/packages/framer-motion/src/render/dom/scroll/track.ts
+++ b/packages/framer-motion/src/render/dom/scroll/track.ts
@@ -72,12 +72,12 @@ export function scrollInfo(
scrollListeners.set(container, listener)
const target = getEventTarget(container)
- window.addEventListener("resize", listener, { passive: true })
+ window.addEventListener("resize", listener)
if (container !== document.documentElement) {
resizeListeners.set(container, resize(container, listener))
}
- target.addEventListener("scroll", listener, { passive: true })
+ target.addEventListener("scroll", listener)
listener()
}
diff --git a/packages/motion-dom/package.json b/packages/motion-dom/package.json
index 370844a669..154a8eff82 100644
--- a/packages/motion-dom/package.json
+++ b/packages/motion-dom/package.json
@@ -1,6 +1,6 @@
{
"name": "motion-dom",
- "version": "12.34.2",
+ "version": "12.34.3",
"author": "Matt Perry",
"license": "MIT",
"repository": "https://github.com/motiondivision/motion",
diff --git a/packages/motion-dom/src/animation/generators/__tests__/spring.test.ts b/packages/motion-dom/src/animation/generators/__tests__/spring.test.ts
index 5fc81e6aec..35eb0c0b34 100644
--- a/packages/motion-dom/src/animation/generators/__tests__/spring.test.ts
+++ b/packages/motion-dom/src/animation/generators/__tests__/spring.test.ts
@@ -141,16 +141,54 @@ describe("spring", () => {
expect(withoutDuration.length).toBeGreaterThan(4)
})
- test("Spring defined as bounce and duration is resolved with correct velocity", () => {
+ test("Time-defined spring ignores velocity", () => {
const settings = {
keyframes: [500, 10],
bounce: 0.2,
duration: 1000,
}
- const resolvedSpring = spring({ ...settings, velocity: 1000 })
+ const withVelocity = spring({ ...settings, velocity: 1000 })
+ const withoutVelocity = spring(settings)
- expect(resolvedSpring.next(0).value).toBe(500)
- expect(Math.floor(resolvedSpring.next(100).value)).toBe(420)
+ // Time-defined springs ignore velocity to prevent wild oscillation
+ // from interrupted animations
+ expect(withVelocity.next(0).value).toBe(withoutVelocity.next(0).value)
+ expect(withVelocity.next(100).value).toBe(
+ withoutVelocity.next(100).value
+ )
+ })
+
+ test("Time-defined spring with velocity does not wildly oscillate", () => {
+ /**
+ * Time-defined springs (duration/bounce) must ignore inherited
+ * velocity. When an animation is interrupted, the motionValue
+ * carries velocity from the in-progress animation. If this leaks
+ * into findSpring(), it changes the computed spring parameters
+ * and causes massive oscillation on small-range animations.
+ */
+ const settings = {
+ keyframes: [0, 100],
+ bounce: 0.2,
+ duration: 400,
+ }
+
+ const noVelocity = spring(settings)
+ const withVelocity = spring({ ...settings, velocity: 5000 })
+
+ let maxNoVelocity = 0
+ let maxWithVelocity = 0
+
+ for (let t = 0; t <= 400; t += 5) {
+ const noVel = noVelocity.next(t).value
+ const withVel = withVelocity.next(t).value
+
+ if (noVel > maxNoVelocity) maxNoVelocity = noVel
+ if (withVel > maxWithVelocity) maxWithVelocity = withVel
+ }
+
+ // Both should have identical mild overshoot (velocity is ignored)
+ expect(maxNoVelocity - 100).toBeLessThan(5)
+ expect(maxWithVelocity - 100).toBeLessThan(5)
})
test("Spring animating back to same number returns correct duration", () => {
diff --git a/packages/motion-dom/src/animation/generators/spring/index.ts b/packages/motion-dom/src/animation/generators/spring/index.ts
index 798edbb5ad..faa17eefaf 100644
--- a/packages/motion-dom/src/animation/generators/spring/index.ts
+++ b/packages/motion-dom/src/animation/generators/spring/index.ts
@@ -41,6 +41,12 @@ function getSpringOptions(options: SpringOptions) {
!isSpringType(options, physicsKeys) &&
isSpringType(options, durationKeys)
) {
+ // Time-defined springs should ignore inherited velocity.
+ // Velocity from interrupted animations can cause findSpring()
+ // to compute wildly different spring parameters, leading to
+ // massive oscillation on small-range animations.
+ springOptions.velocity = 0
+
if (options.visualDuration) {
const visualDuration = options.visualDuration
const root = (2 * Math.PI) / (visualDuration * 1.2)
@@ -57,7 +63,7 @@ function getSpringOptions(options: SpringOptions) {
damping,
}
} else {
- const derived = findSpring(options)
+ const derived = findSpring({ ...options, velocity: 0 })
springOptions = {
...springOptions,
diff --git a/packages/motion/package.json b/packages/motion/package.json
index 69bf7ede67..b9ce4bbee9 100644
--- a/packages/motion/package.json
+++ b/packages/motion/package.json
@@ -1,6 +1,6 @@
{
"name": "motion",
- "version": "12.34.2",
+ "version": "12.34.3",
"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.34.2",
+ "framer-motion": "^12.34.3",
"tslib": "^2.4.0"
},
"peerDependencies": {
diff --git a/yarn.lock b/yarn.lock
index f59f09d3ed..1d66f597ae 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7437,14 +7437,14 @@ __metadata:
languageName: node
linkType: hard
-"framer-motion@^12.34.2, framer-motion@workspace:packages/framer-motion":
+"framer-motion@^12.34.3, 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.34.2
+ motion-dom: ^12.34.3
motion-utils: ^12.29.2
three: 0.137.0
tslib: ^2.4.0
@@ -8209,9 +8209,9 @@ __metadata:
version: 0.0.0-use.local
resolution: "html-env@workspace:dev/html"
dependencies:
- framer-motion: ^12.34.2
- motion: ^12.34.2
- motion-dom: ^12.34.2
+ framer-motion: ^12.34.3
+ motion: ^12.34.3
+ motion-dom: ^12.34.3
react: ^18.3.1
react-dom: ^18.3.1
vite: ^5.2.0
@@ -10953,7 +10953,7 @@ __metadata:
languageName: node
linkType: hard
-"motion-dom@^12.34.2, motion-dom@workspace:packages/motion-dom":
+"motion-dom@^12.34.3, motion-dom@workspace:packages/motion-dom":
version: 0.0.0-use.local
resolution: "motion-dom@workspace:packages/motion-dom"
dependencies:
@@ -11032,11 +11032,11 @@ __metadata:
languageName: unknown
linkType: soft
-"motion@^12.34.2, motion@workspace:packages/motion":
+"motion@^12.34.3, motion@workspace:packages/motion":
version: 0.0.0-use.local
resolution: "motion@workspace:packages/motion"
dependencies:
- framer-motion: ^12.34.2
+ framer-motion: ^12.34.3
tslib: ^2.4.0
peerDependencies:
"@emotion/is-prop-valid": "*"
@@ -11153,7 +11153,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "next-env@workspace:dev/next"
dependencies:
- motion: ^12.34.2
+ motion: ^12.34.3
next: 15.5.10
react: 19.0.0
react-dom: 19.0.0
@@ -12625,7 +12625,7 @@ __metadata:
"@typescript-eslint/parser": ^7.2.0
"@vitejs/plugin-react-swc": ^3.5.0
eslint-plugin-react-refresh: ^0.4.6
- motion: ^12.34.2
+ motion: ^12.34.3
react: ^19.0.0
react-dom: ^19.0.0
vite: ^5.2.0
@@ -12709,7 +12709,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.34.2
+ framer-motion: ^12.34.3
react: ^18.3.1
react-dom: ^18.3.1
vite: ^5.2.0