diff --git a/dev/html/package.json b/dev/html/package.json
index 3d815866f6..f0f16bfc7c 100644
--- a/dev/html/package.json
+++ b/dev/html/package.json
@@ -1,7 +1,7 @@
{
"name": "html-env",
"private": true,
- "version": "12.33.1",
+ "version": "12.34.0-alpha.0",
"type": "module",
"scripts": {
"dev": "vite",
@@ -10,9 +10,9 @@
"preview": "vite preview"
},
"dependencies": {
- "framer-motion": "^12.33.1",
- "motion": "^12.33.1",
- "motion-dom": "^12.33.1",
+ "framer-motion": "^12.34.0-alpha.0",
+ "motion": "^12.34.0-alpha.0",
+ "motion-dom": "^12.34.0-alpha.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
diff --git a/dev/next/package.json b/dev/next/package.json
index 61da8f8a81..26cb30f1c5 100644
--- a/dev/next/package.json
+++ b/dev/next/package.json
@@ -1,7 +1,7 @@
{
"name": "next-env",
"private": true,
- "version": "12.33.1",
+ "version": "12.34.0-alpha.0",
"type": "module",
"scripts": {
"dev": "next dev",
@@ -10,7 +10,7 @@
"build": "next build"
},
"dependencies": {
- "motion": "^12.33.1",
+ "motion": "^12.34.0-alpha.0",
"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 996031615a..1c451f7522 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.33.1",
+ "version": "12.34.0-alpha.0",
"type": "module",
"scripts": {
"dev": "vite",
@@ -11,7 +11,7 @@
"preview": "vite preview"
},
"dependencies": {
- "motion": "^12.33.1",
+ "motion": "^12.34.0-alpha.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
diff --git a/dev/react/package.json b/dev/react/package.json
index a8baa0f678..c08a7281b3 100644
--- a/dev/react/package.json
+++ b/dev/react/package.json
@@ -1,7 +1,7 @@
{
"name": "react-env",
"private": true,
- "version": "12.33.1",
+ "version": "12.34.0-alpha.0",
"type": "module",
"scripts": {
"dev": "yarn vite",
@@ -11,7 +11,7 @@
"preview": "yarn vite preview"
},
"dependencies": {
- "framer-motion": "^12.33.1",
+ "framer-motion": "^12.34.0-alpha.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
diff --git a/dev/react/src/tests/manual-frame-control.tsx b/dev/react/src/tests/manual-frame-control.tsx
new file mode 100644
index 0000000000..f53af36eda
--- /dev/null
+++ b/dev/react/src/tests/manual-frame-control.tsx
@@ -0,0 +1,318 @@
+import { motion, useMotionValue } from "framer-motion"
+import { renderFrame } from "motion-dom"
+import { MotionGlobalConfig } from "motion-utils"
+import { useCallback, useEffect, useState } from "react"
+
+/**
+ * Demo: Manual Frame Control
+ *
+ * This demonstrates how to manually control Motion animations frame-by-frame,
+ * useful for:
+ * - Remotion video rendering
+ * - WebXR immersive sessions
+ * - Debugging animations step-by-step
+ * - Creating scrubable animation timelines
+ */
+export const App = () => {
+ const [manualMode, setManualMode] = useState(false)
+ const [currentFrame, setCurrentFrame] = useState(0)
+ const [fps] = useState(30)
+ const [isAnimating, setIsAnimating] = useState(false)
+ const x = useMotionValue(0)
+
+ // Calculate current time in ms
+ const currentTime = (currentFrame / fps) * 1000
+
+ // Enable/disable manual timing mode
+ useEffect(() => {
+ if (manualMode) {
+ MotionGlobalConfig.useManualTiming = true
+ // Reset to frame 0 when entering manual mode
+ setCurrentFrame(0)
+ renderFrame({ frame: 0, fps })
+ } else {
+ MotionGlobalConfig.useManualTiming = undefined
+ }
+ return () => {
+ MotionGlobalConfig.useManualTiming = undefined
+ }
+ }, [manualMode, fps])
+
+ // Render the current frame when it changes (in manual mode)
+ useEffect(() => {
+ if (manualMode) {
+ renderFrame({ frame: currentFrame, fps })
+ }
+ }, [currentFrame, manualMode, fps])
+
+ const nextFrame = useCallback(() => {
+ setCurrentFrame((f) => f + 1)
+ }, [])
+
+ const prevFrame = useCallback(() => {
+ setCurrentFrame((f) => Math.max(0, f - 1))
+ }, [])
+
+ const goToFrame = useCallback((frame: number) => {
+ setCurrentFrame(Math.max(0, frame))
+ }, [])
+
+ const toggleAnimation = useCallback(() => {
+ setIsAnimating((a) => !a)
+ }, [])
+
+ return (
+
+
Manual Frame Control Demo
+
+ {/* Controls */}
+
+
+
+ {manualMode && (
+ <>
+
+
+
+
+
+
+
+
+ goToFrame(parseInt(e.target.value))
+ }
+ style={sliderStyle}
+ id="frame-slider"
+ />
+
+
+
+
+
+
+
+ >
+ )}
+
+
+
+
+ {/* Animated element */}
+
+
{
+ if (!manualMode) {
+ console.log("Animation complete!")
+ }
+ }}
+ />
+
+ {/* Position indicator */}
+
+ X Position: {Math.round(x.get())}px
+
+
+
+
+
+
+
+ {/* Info panel */}
+
+
How it works:
+
+ -
+ Manual Timing Mode: Disables
+ requestAnimationFrame-based updates
+
+ -
+ Next/Prev Frame: Advance or rewind by
+ one frame (~33ms at 30fps)
+
+ -
+ Frame Slider: Scrub through the
+ animation timeline
+
+ -
+ FPS: {fps} frames per second (
+ {(1000 / fps).toFixed(1)}ms per frame)
+
+
+
+
Remotion Integration:
+
+ {`import { MotionRemotion } from 'motion-remotion'
+
+function MyComposition() {
+ return (
+
+
+
+ )
+}`}
+
+
+
+ )
+}
+
+const containerStyle: React.CSSProperties = {
+ padding: 20,
+ fontFamily: "system-ui, sans-serif",
+ maxWidth: 800,
+ margin: "0 auto",
+}
+
+const titleStyle: React.CSSProperties = {
+ marginBottom: 20,
+}
+
+const controlsStyle: React.CSSProperties = {
+ display: "flex",
+ flexDirection: "column",
+ gap: 15,
+ marginBottom: 30,
+ padding: 20,
+ backgroundColor: "#f5f5f5",
+ borderRadius: 8,
+}
+
+const labelStyle: React.CSSProperties = {
+ display: "flex",
+ alignItems: "center",
+ gap: 8,
+ fontSize: 16,
+ cursor: "pointer",
+}
+
+const buttonGroupStyle: React.CSSProperties = {
+ display: "flex",
+ gap: 10,
+}
+
+const buttonStyle: React.CSSProperties = {
+ padding: "10px 20px",
+ fontSize: 14,
+ cursor: "pointer",
+ border: "none",
+ borderRadius: 4,
+ backgroundColor: "#2196F3",
+ color: "white",
+}
+
+const sliderContainerStyle: React.CSSProperties = {
+ display: "flex",
+ flexDirection: "column",
+ gap: 5,
+}
+
+const sliderStyle: React.CSSProperties = {
+ width: "100%",
+}
+
+const stageStyle: React.CSSProperties = {
+ position: "relative",
+ height: 150,
+ backgroundColor: "#1a1a2e",
+ borderRadius: 8,
+ marginBottom: 20,
+ overflow: "hidden",
+}
+
+const boxStyle: React.CSSProperties = {
+ position: "absolute",
+ top: 25,
+ left: 20,
+ width: 100,
+ height: 100,
+ backgroundColor: "#e94560",
+ borderRadius: 8,
+}
+
+const indicatorStyle: React.CSSProperties = {
+ position: "absolute",
+ bottom: 10,
+ left: 20,
+ color: "white",
+ fontSize: 14,
+}
+
+const infoStyle: React.CSSProperties = {
+ padding: 20,
+ backgroundColor: "#f9f9f9",
+ borderRadius: 8,
+}
+
+const codeStyle: React.CSSProperties = {
+ backgroundColor: "#1a1a2e",
+ color: "#e94560",
+ padding: 15,
+ borderRadius: 4,
+ overflow: "auto",
+ fontSize: 13,
+}
diff --git a/lerna.json b/lerna.json
index d1f0925b41..5eb569362f 100644
--- a/lerna.json
+++ b/lerna.json
@@ -1,5 +1,5 @@
{
- "version": "12.33.1",
+ "version": "12.34.0-alpha.0",
"packages": [
"packages/*",
"dev/*"
diff --git a/packages/framer-motion/package.json b/packages/framer-motion/package.json
index 6da9f86767..8fa737ca84 100644
--- a/packages/framer-motion/package.json
+++ b/packages/framer-motion/package.json
@@ -1,6 +1,6 @@
{
"name": "framer-motion",
- "version": "12.33.1",
+ "version": "12.34.0-alpha.0",
"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.33.1",
+ "motion-dom": "^12.34.0-alpha.0",
"motion-utils": "^12.29.2",
"tslib": "^2.4.0"
},
diff --git a/packages/framer-motion/src/utils/__tests__/use-manual-frame-remotion.test.tsx b/packages/framer-motion/src/utils/__tests__/use-manual-frame-remotion.test.tsx
new file mode 100644
index 0000000000..10232ce0c9
--- /dev/null
+++ b/packages/framer-motion/src/utils/__tests__/use-manual-frame-remotion.test.tsx
@@ -0,0 +1,1764 @@
+/**
+ * Remotion Integration Tests
+ *
+ * In production, wrap your composition with `` from `motion-remotion`.
+ */
+
+import { motionValue, Variants, renderFrame } from "motion-dom"
+import { MotionGlobalConfig } from "motion-utils"
+import { createContext, useContext, ReactNode, useInsertionEffect, useLayoutEffect, useRef } from "react"
+import { act } from "react"
+import { motion, AnimatePresence } from "../../"
+import { render } from "../../jest.setup"
+
+// Mock Remotion API
+interface VideoConfig {
+ fps: number
+ width: number
+ height: number
+ durationInFrames: number
+ id: string
+}
+
+interface RemotionContextValue {
+ frame: number
+ config: VideoConfig
+}
+
+const RemotionContext = createContext(null)
+
+function useCurrentFrame(): number {
+ const ctx = useContext(RemotionContext)
+ if (!ctx) throw new Error("useCurrentFrame must be used within Remotion context")
+ return ctx.frame
+}
+
+function useVideoConfig(): VideoConfig {
+ const ctx = useContext(RemotionContext)
+ if (!ctx) throw new Error("useVideoConfig must be used within Remotion context")
+ return ctx.config
+}
+
+function Sequence({
+ from = 0,
+ children,
+}: {
+ from?: number
+ durationInFrames?: number
+ children: ReactNode
+}) {
+ const parentFrame = useCurrentFrame()
+ const config = useVideoConfig()
+ const relativeFrame = parentFrame - from
+
+ return (
+
+ {relativeFrame >= 0 ? children : null}
+
+ )
+}
+
+function AbsoluteFill({ children, style }: { children: ReactNode; style?: React.CSSProperties }) {
+ return (
+
+ {children}
+
+ )
+}
+
+/**
+ * Test bridge that mirrors MotionRemotion from the plus repo.
+ * Uses renderFrame with style-capture for backward scrubbing.
+ */
+function MotionRemotionBridge({ children }: { children: ReactNode }) {
+ const currentFrame = useCurrentFrame()
+ const { fps } = useVideoConfig()
+ const prevFrame = useRef(-1)
+ const containerRef = useRef(null)
+ const styleCacheRef = useRef