| name | video-coder |
| description | Expert React video scene component creator for educational content. Builds production-grade, visually distinctive components using framer-motion animations, pixel-precise positioning, and optimized performance patterns. Follows strict component format with React.memo, threshold-based state updates, and module-level definitions. Outputs self-contained TSX components with proper timing sync, 60fps performance, and comprehensive reference-based implementation. |
Video Coder
When the design includes elements with type: "asset", you'll receive an asset_manifest with entries like:
This document defines the required format for React video scene components.
export default Scene{N};
Where `{N}` is the scene number (e.g., `Scene0`, `Scene1`, `Scene2`)
`currentTime` is the global value of time with respect to the video start.
</export-pattern>
</required-structure>
---
<sub-components>
### Sub-Components (CRITICAL)
**All sub-components MUST use `React.memo`** and be defined at module level (outside the main Scene component).
<react-memo>
#### Why React.memo is Required
- Video components re-render 60 times per second as `currentTime` changes
- Without `React.memo`, sub-components re-render unnecessarily causing animation jitter
- Module-level definitions ensure stable references across renders
</react-memo>
<sub-component-pattern>
// CORRECT: Module-level with React.memo
const TreeNode = React.memo(({
value,
position,
isVisible
}: {
value: string;
position: { x: number; y: number };
isVisible: boolean;
}) => (
<motion.div animate={isVisible ? "visible" : "hidden"}>
{value}
</motion.div>
));
// WRONG: Defined inside component (causes jitter)
export default function Scene0({ currentTime }: SceneProps) {
// ❌ Never define components here
const TreeNode = ({ value }) => <div>{value}</div>;
}
</sub-component-pattern>
<module-level-definitions>
#### What Goes at Module Level (Outside Component)
1. **Sub-components** - Always wrapped with `React.memo`
2. **Animation variants** - Objects defining animation states
3. **Static data** - Positions, configurations that don't change
```typescript
// Animation variants at module level
const fadeVariants = {
hidden: { opacity: 0 },
visible: { opacity: 1, transition: { duration: 0.5 } }
};
// Static positions at module level
const nodePositions = {
node1: { x: 576, y: 540 },
node2: { x: 1344, y: 540 }
};
// Sub-component at module level with React.memo
const InfoCard = React.memo(({ title, isVisible }: { title: string; isVisible: boolean }) => (
<motion.div variants={fadeVariants} animate={isVisible ? "visible" : "hidden"}>
{title}
</motion.div>
));
import React, { useMemo } from 'react';
import { motion } from 'framer-motion';
interface SceneProps {
currentTime: number;
}
// Animation variants at module level
const nodeVariants = {
hidden: { scale: 0, opacity: 0 },
visible: { scale: 1, opacity: 1, transition: { duration: 0.4 } }
};
// Static data at module level
const nodePositions = {
node1: { x: 576, y: 540 },
node2: { x: 1344, y: 540 }
};
// Sub-component at module level with React.memo
const TreeNode = React.memo(({
value,
position,
isVisible
}: {
value: string;
position: { x: number; y: number };
isVisible: boolean;
}) => (
<div
className="absolute -translate-x-1/2 -translate-y-1/2"
style={{ left: `${position.x}px`, top: `${position.y}px` }}
>
<motion.div
variants={nodeVariants}
initial="hidden"
animate={isVisible ? "visible" : "hidden"}
className="w-20 h-20 rounded-full bg-white flex items-center justify-center"
>
{value}
</motion.div>
</div>
));
// Main Scene component with React.memo
const Scene0 = React.memo(function Scene0({ currentTime }: SceneProps) {
// Threshold-based state updates
const states = useMemo(() => ({
showNode1: currentTime >= 1000,
showNode2: currentTime >= 2000,
}), [Math.floor(currentTime / 250)]);
return (
<div className="relative w-full h-full bg-gray-900">
<TreeNode value="A" position={nodePositions.node1} isVisible={states.showNode1} />
<TreeNode value="B" position={nodePositions.node2} isVisible={states.showNode2} />
</div>
);
});
export default Scene0;
Arbitrary values let you insert any exact CSS value inside a Tailwind class using square brackets, giving you full freedom beyond Tailwind's default scales.
What They Do
Let you set custom sizes, spacing, colors, borders, radii, typography, and positioning instantly.
CRITICAL: Follow these patterns to prevent animation jittering and re-rendering issues.
React video components re-render up to 60 times per second. Unstable references cause animations to restart, creating visual jitter.
Define sub-components, animation variants, and static data outside the parent component for stable references.
// Animation variants
const nodeVariants = {
hidden: { scale: 0, opacity: 0 },
visible: { scale: 1, opacity: 1, transition: { duration: 0.4 } }
};
// Static data (positions in pixels, styles)
const nodePositions = {
node1: { x: 576, y: 540 },
node2: { x: 740, y: 540 },
};
// Sub-component with React.memo
const TreeNode = React.memo(({
value,
position, // Position in pixels
isVisible
}: {
value: string;
position: { x: number; y: number };
isVisible: boolean;
}) => (
<motion.div
variants={nodeVariants}
initial="hidden"
animate={isVisible ? "visible" : "hidden"}
>
{value}
</motion.div>
));
Update states every 42ms using Math.floor(currentTime / 42) to prevent excessive re-renders while matching 24fps video output.
// State updates inside components
const states = useMemo(() => ({
showTitle: currentTime >= 1000,
showGrid: currentTime >= 2000,
fadeOut: currentTime >= 9000
}), [Math.floor(currentTime / 42)]);
// Computed collections inside components
const visibleItems = useMemo(() => {
const visible = new Set<string>();
if (currentTime >= 1000) visible.add('item1');
if (currentTime >= 2000) visible.add('item2');
return visible;
}, [Math.floor(currentTime / 42)]);
// Static data created once at mount
const particles = useMemo(() =>
Array.from({ length: 40 }, () => ({
x: Math.random() * 100,
y: Math.random() * 100
})),
[] // Empty deps = created once
);
Pass all dependencies as explicit props for React.memo to work correctly.
const TreeNode = React.memo(({
value,
showTree // Explicit prop, not derived from currentTime inside
}: {
value: string;
showTree: boolean;
}) => (
<motion.div animate={showTree ? "visible" : "hidden"} />
));
// In parent: derive state, pass as prop
<TreeNode value="50" showTree={states.showTree} />
import React, { useMemo } from 'react';
import { motion } from 'framer-motion';
interface SceneProps {
currentTime: number;
}
const nodeVariants = {
hidden: { scale: 0, opacity: 0 },
visible: { scale: 1, opacity: 1, transition: { duration: 0.4 } }
};
const nodePositions = {
node1: { x: 576, y: 540 },
node2: { x: 1344, y: 540 },
};
const TreeNode = React.memo(({
value,
position, // Position in pixels
isVisible
}: {
value: string;
position: { x: number; y: number };
isVisible: boolean;
}) => (
<div
className="absolute -translate-x-1/2 -translate-y-1/2"
style={{ left: `${position.x}px`, top: `${position.y}px` }}
>
<motion.div
variants={nodeVariants}
initial="hidden"
animate={isVisible ? "visible" : "hidden"}
className="w-20 h-20 rounded-full bg-white flex items-center justify-center"
>
{value}
</motion.div>
</div>
));
// Main Scene component with React.memo
const Scene0 = React.memo(function Scene0({ currentTime }: SceneProps) {
const states = useMemo(() => ({
showNode1: currentTime >= 1000,
showNode2: currentTime >= 2000,
}), [Math.floor(currentTime / 42)]);
return (
<div className="relative w-full h-full bg-gray-900">
<TreeNode value="A" position={nodePositions.node1} isVisible={states.showNode1} />
<TreeNode value="B" position={nodePositions.node2} isVisible={states.showNode2} />
</div>
);
});
export default Scene0;
Positioning elements in video scenes using Tailwind CSS and framer-motion.
For ALL positioning in video scenes, use this consistent pattern with pixel values:
A motion.div cannot have absolute positioning, it should be wrapped inside an absolute div that is properly positioned as shown.
FOR SHAPES/TEXT/ICONS: Position: Always refers to element's CENTER point
FOR PATHS: All coordinates are ABSOLUTE screen positions No position/size fields needed (implied by path coordinates)
ROTATION 0° = pointing up (↑) 90° = pointing right (→) 180° = pointing down (↓) 270° = pointing left (←)
Positive values = clockwise rotation Negative values = counter-clockwise (-90° same as 270°)
EXAMPLE (1920×1080 viewport) Screen center: x = 960, y = 540 Top-center: x = 960, y = 100 Bottom-left quadrant: x = 480, y = 810 Right edge center: x = 1820, y = 540
Position at any pixel value using the same pattern:
Without -translate-y-1/2, the element's top edge sits at the pixel value, causing overlaps when elements have varying heights. Full centering positions the element's center at the pixel coordinate, ensuring safe spacing.
Layer elements using Tailwind's z-[index] utilities:
{/* Content layer - Landscape center: 540px, 960px */}
{/* Overlay layer - Landscape: 216px from top, 1536px from left */}
Be thorough in studying any animation pattern you're using in your scene.
| Type | Description | Use Case |
|---|---|---|
| Tween | Duration-based, precise timing | Coordinated animations, sync with audio |
| Spring | Physics-based, bounce/elasticity | Interactive UI, natural motion |
| Inertia | Momentum-based deceleration | Drag interactions, swipe gestures |
transition={{
duration?: number, // Seconds (default: 0.3)
ease?: string | array, // Easing function (default: "easeInOut")
delay?: number, // Delay in seconds
repeat?: number, // Number of repeats (Infinity for loop)
repeatType?: "loop" | "reverse" | "mirror",
times?: number[], // Keyframe timing [0, 0.5, 1]
}}
| Ease | Behavior | Use Case |
|---|---|---|
linear |
Constant speed | Mechanical motion, loading indicators |
easeIn |
Slow → fast | Exit animations, falling objects |
easeOut |
Fast → slow | Entrances, coming to rest |
easeInOut |
Slow → fast → slow | Default for most UI animations |
circIn/Out/InOut |
Sharper circular curve | Snappy, aggressive motion |
backIn |
Pulls back, then forward | Anticipation effects |
backOut |
Overshoots, then settles | Bouncy clicks, attention-grabbing |
backInOut |
Both effects combined | Playful, game UI |
anticipate |
Dramatic pullback | Hero entrances, launch effects |
steps(n) |
Discrete steps | Pixel art, frame-by-frame |
Only supports 2 keyframes (from → to).
Note: Cannot mix bounce with stiffness/damping/mass.
// Snappy transition={{ type: "spring", stiffness: 400, damping: 30 }}
// Soft transition={{ type: "spring", stiffness: 60, damping: 10 }}
// Anti-clockwise rotation (negative degrees) <motion.div animate={{ rotate: -90 }} transition={{ duration: 1 }} />
// Continuous clockwise spin <motion.div animate={{ rotate: 360 }} transition={{ duration: 2, repeat: Infinity, ease: "linear" }} />
// Continuous anti-clockwise spin <motion.div animate={{ rotate: -360 }} transition={{ duration: 2, repeat: Infinity, ease: "linear" }} />
// Custom pivot point <motion.div style={{ transformOrigin: "top left" }} animate={{ rotate: 45 }} />
For animating elements along SVG paths, see the dedicated path-following.md.
transition={{
x: { type: "spring", stiffness: 200 },
opacity: { duration: 0.5, ease: "easeOut" },
scale: { type: "spring", bounce: 0.6 }
}}