7.3 KiB
name, description, user-invocable
| name | description | user-invocable |
|---|---|---|
| animation-translation | Library mapping from source-site animation primitives (CSS keyframes, CSS transitions, GSAP tweens/timelines/ScrollTrigger, Framer Motion, Lottie, Three.js, IntersectionObserver reveals) to target primitives in the cloned Next.js project. Rules on when to translate vs keep source library. Load this in Stage 2 (CSS portion) and Stage 3 (JS portion). | false |
Animation Translation
Rule of thumb: prefer declarative (Framer Motion, Tailwind transitions) over imperative (GSAP, manual requestAnimationFrame) when the translation is clean. Keep imperative when translation loses fidelity — e.g. a GSAP timeline with 15 chained steps.
Mapping table
| Source | Target | Stage | Notes |
|---|---|---|---|
CSS keyframes (@keyframes) |
CSS keyframes kept in globals.css |
2 | No reason to translate |
| CSS transition | Tailwind transition-* / arbitrary |
2 | transition-colors duration-200 ease-in-out |
| GSAP tween (single property) | Framer Motion animate prop |
3 | <motion.div animate={{ opacity: 1 }} transition={{...}}/> |
| GSAP timeline (complex, chained) | Keep GSAP + @gsap/react useGSAP |
3 | Do not force bad Framer translation |
| GSAP ScrollTrigger (simple) | Framer useScroll + useTransform |
3 | Only when trigger + 1-2 output props |
| GSAP ScrollTrigger (complex) | Keep GSAP + ScrollTrigger | 3 | Timeline > 3 steps, pinned, scrub |
| Framer Motion (source already) | Framer Motion direct | 3 | Copy variants, transition, whileInView verbatim |
| Lottie | lottie-react + captured animationData |
3 | Component is "use client" |
| Three.js | @react-three/fiber + drei |
4 | Handled by clone-advanced |
| Custom GLSL shaders | <shaderMaterial> with extracted GLSL |
4 | Handled by clone-advanced |
IntersectionObserver reveal |
Framer whileInView + viewport: { once: true } |
3 | Rarely worth keeping imperative |
Raw <canvas> 2D loop |
Keep the <canvas> + "use client" + useEffect |
4 | If reconstruction fails, video fallback |
WAAPI (Element.animate()) |
Translate to Framer animate |
3 | Similar semantics |
Parallax (background-attachment: fixed) |
CSS kept | 2 | Browsers support it directly |
Stage 2 (CSS animation)
Keyframes
Source:
@keyframes fadeUp {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: none;
}
}
.hero-title {
animation: fadeUp 0.6s ease-out forwards;
}
Target — in globals.css:
@layer utilities {
@keyframes fade-up {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: none;
}
}
.animate-fade-up {
animation: fade-up 0.6s ease-out forwards;
}
}
Or, per css-to-tailwind, add to tailwind.config.ts:
theme: { extend: {
keyframes: { "fade-up": { from: { opacity: "0", transform: "translateY(12px)" }, to: { opacity: "1", transform: "none" } } },
animation: { "fade-up": "fade-up 0.6s ease-out forwards" },
}}
Then the component just has className="animate-fade-up".
Transitions
Source: transition: transform 400ms cubic-bezier(0.22,1,0.36,1);
Target: className="transition-transform duration-[400ms] ease-[cubic-bezier(0.22,1,0.36,1)]". If that tuple repeats, promote the easing to theme.extend.transitionTimingFunction.
Stage 3 (JS animation)
Framer Motion (common path)
The capture hook dump gives you the component's variants, transition, animate, initial, whileInView, whileHover, whileTap verbatim. Reproduce them:
'use client';
import { motion } from 'framer-motion';
const variants = { hidden: { opacity: 0, y: 12 }, visible: { opacity: 1, y: 0 } };
export default function Hero() {
return (
<motion.section
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.3 }}
variants={variants}
transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}
>
...
</motion.section>
);
}
GSAP ScrollTrigger kept
When the source has a pinned, scrubbed, multi-step timeline, translating to Framer would flatten it. Keep GSAP:
'use client';
import { useRef } from 'react';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import { useGSAP } from '@gsap/react';
gsap.registerPlugin(ScrollTrigger);
export default function PinnedHero() {
const ref = useRef<HTMLDivElement>(null);
useGSAP(
() => {
const tl = gsap.timeline({
scrollTrigger: { trigger: ref.current, start: 'top top', end: '+=1200', pin: true, scrub: 1 },
});
tl.to('.layer-a', { yPercent: -30 }).to('.layer-b', { yPercent: -50 }, '<');
},
{ scope: ref }
);
return <div ref={ref}>...</div>;
}
Lottie
'use client';
import Lottie from 'lottie-react';
import animationData from '@/public/assets/cloned/lottie/<hash>.json';
export default function LottieIcon() {
return <Lottie animationData={animationData} loop autoplay style={{ width: 64, height: 64 }} />;
}
Prefer imports over fetch() so the JSON ships in the bundle and there is no layout shift on first play.
Respecting prefers-reduced-motion
The Stage 1 gate runs with reduced motion forced, so your Stage 1 output should render a static final-state snapshot when motion is reduced.
In Framer Motion, use the MotionConfig with reducedMotion="user" in layout.tsx, or gate animate / initial on a useReducedMotion() hook.
For CSS animations, wrap the rule:
@media (prefers-reduced-motion: no-preference) {
.animate-fade-up {
animation: fade-up 0.6s ease-out forwards;
}
}
Do not
- Do not add animations that are not in the source. The clone target is fidelity, not flair.
- Do not approximate a GSAP timeline with a Framer
animatesequence if the timeline has overlaps, labels, or scrubbing — keep GSAP. - Do not use
requestAnimationFrameby hand if Framer covers the case. You will burn React lifecycle bugs. - Do not import full
gsapbundle if onlyScrollTriggeris needed — it is fine toimport { gsap } from "gsap"because tree-shaking handles it, but check the bundle size if multiple sections use GSAP.