What is motion-advanced?
Advanced motion patterns for React / Next. js — drag & drop, gestures, text animations, SVG path drawing, custom hooks, imperative sequences (useAnimate), loaders, and the full API decision tree.
affaan-m/ECC
Advanced motion patterns for React / Next.js — drag & drop, gestures, text animations, SVG path drawing, custom hooks, imperative sequences (useAnimate), loaders, and the full API decision tree. Requires motion-foundations.
npx skills add https://github.com/affaan-m/ECC --skill "skills/motion-advanced"Quick start
Install it or open the source, trigger it with a clear task, then follow the source workflow.
npx skills add https://github.com/affaan-m/ECC --skill "skills/motion-advanced"Use motion-advanced to help me with: [describe your task]. Before you begin, tell me what input you need, the steps you will follow, and the expected output.
No structured workflow was detected; follow the original SKILL.md below.
Continue to the workflowDirect answers
Advanced motion patterns for React / Next. js — drag & drop, gestures, text animations, SVG path drawing, custom hooks, imperative sequences (useAnimate), loaders, and the full API decision tree.
It is relevant to workflows involving Engineering, Next.js, React.
SkillSignal detected this source-specific command: npx skills add https://github.com/affaan-m/ECC --skill "skills/motion-advanced". Inspect the repository and command before running it.
cursor
No obvious permission action was detected by the static rules. This is not proof that the Skill is safe.
This page combines upstream documentation with deterministic repository, quality, and static-risk signals. It is not described as a manual test or security review.
SkillSignal brief
Advanced motion patterns for React / Next. js — drag & drop, gestures, text animations, SVG path drawing, custom hooks, imperative sequences (useAnimate), loaders, and the full API decision tree.
Useful in these contexts
Core capabilities
Quality breakdown
Based on traceable docs and repository signals; stars are not treated as quality.
Compare before choosing
These links are selected from shared tasks, functions, stacks, platforms, and same-name variants. Compare the source owner, documentation, permissions, and maintenance signals.
React and Next.js performance optimization patterns adapted from Vercel Engineering's React Best Practices (https://github.com/vercel-labs/agent-skills). Organizes 70+ rules across 8 priority categories — waterfalls, bundle size, server-side, client fetching, re-render, rendering, JS micro-perf, advanced. Use when writing, reviewing, or refactoring React/Next.js code for performance.
Production-ready animation patterns for React / Next.js — button, modal, toast, stagger, page transitions, exit animations, scroll, and layout — built on motion-foundations tokens and springs.
Install and configure Markstream streaming Markdown renderers for Vue, React, Svelte, Angular, Nuxt, and Vue 2 applications. Use for package selection, minimal peer dependencies, CSS order, SSR boundaries, streaming mode, and renderer setup.
Guide for implementing smooth, native-feeling animations using React's View Transition API (`<ViewTransition>` component, `addTransitionType`, and CSS view transition pseudo-elements). Use this skill whenever the user wants to add page transitions, animate route changes, create shared element animations, animate enter/exit of components, animate list reorder, implement directional (forward/back) navigation animations, or integrate view transitions in Next.js. Also use when the user mentions view
Find the bugs a diff can actually ship: TypeScript, React, Next.js, OWASP, accessibility, SEO, database, and deploy-risk review. Return findings, not a grade.
Complex, interactive, and physics-based animation patterns.
Requires motion-foundations to be set up first.
Use these when motion-patterns is not enough.
useScrollReveal, magnetic button, cursor follower)useAnimateThis skill produces:
Reorder.Group listsuseScrollReveal, useHoverScale, useNavigationDirection, useInViewOnceuseAnimate with interrupt-safe async/awaituseSpring, springs.*) always feels more natural than duration-based for direct manipulation.useMotionValue + useTransform computes derived values without triggering re-renders.useAnimate sequences are imperative and interrupt-safe — calling animate() mid-flight cancels the previous animation automatically.useMotionValue, useSpring) are SSR-safe and do not cause hydration errors.drag prop works on both but feel and threshold differ.document.visibilityState === "hidden". Background tabs must not consume GPU/CPU.offset + velocity checks.useAnimate scope ref must be attached to a mounted DOM element. Calling animate() before mount throws silently.useMotionValue(0) inside a component body is correct; new MotionValue(0) in a render is not.motion-foundations. No inline numbers.window.addEventListener needs a matching removeEventListener in the useEffect return.| Scenario | API |
|---|---|
| Drag with physics on release | drag + dragTransition: springs.release |
| Ordered drag-to-reorder list | Reorder.Group + Reorder.Item |
| Dismiss on drag offset | drag="y" + onDragEnd offset check |
| Swipe left/right | drag="x" + onDragEnd offset check |
| Long press | useLongPress hook |
| Value smoothed over time | useSpring |
| Value derived from another | useTransform |
| Multi-step sequence | useAnimate with async/await |
| One-shot imperative animation | animate() from motion |
| Text entering word by word | Stagger on inline-block spans |
| SVG drawing on | pathLength 0 → 1 |
| SVG morph | d attribute tween (equal commands) |
| Circular progress | strokeDashoffset tween |
useSpring vs a spring transitionuseSpring | transition: springs.* | |
|---|---|---|
| Use for | Cursor follower, pointer-tracked values | Discrete state changes |
| Updates | Continuous, on every frame | Triggered by state change |
| Interrupt | Smooth — physics picks up from velocity | Restarts from current value |
Reactive computation without re-renders:
const x = useMotionValue(0)
const opacity = useTransform(x, [-200, 0, 200], [0, 1, 0])
// opacity updates every frame as x changes — no setState, no re-render
Returns [scope, animate]. The scope ref must be attached to a DOM element.
animate() calls are interrupt-safe — calling mid-flight cancels the previous run.
const [scope, animate] = useAnimate()
async function play() {
await animate(".step-1", { opacity: 1 }, { duration: 0.3 })
await animate(".step-2", { x: 0 }, { duration: 0.4 })
animate(".step-3", { scale: 1 }, { duration: 0.25 }) // fire and forget
}
return <div ref={scope}>...</div>
"use client"
import { motion } from "motion/react"
import { springs, motionTokens } from "@/lib/motion-tokens"
<motion.div
drag
dragConstraints={{ left: -100, right: 100, top: -100, bottom: 100 }}
dragElastic={0.1}
whileDrag={{
scale: motionTokens.scale.pop,
boxShadow: "0 16px 40px rgba(0,0,0,0.2)",
}}
dragTransition={springs.release}
/>
"use client"
import { motion, useMotionValue, useTransform } from "motion/react"
export function BottomSheet({ onClose }: { onClose: () => void }) {
const y = useMotionValue(0)
const opacity = useTransform(y, [0, 200], [1, 0])
return (
<motion.div
drag="y"
dragConstraints={{ top: 0 }}
style={{ y, opacity }}
onDragEnd={(_, info) => {
// Rule 3: combine offset + velocity
if (info.offset.y > 120 || info.velocity.y > 500) onClose()
}}
/>
)
}
"use client"
import { Reorder } from "motion/react"
export function SortableList() {
const [items, setItems] = useState(initialItems)
return (
<Reorder.Group axis="y" values={items} onReorder={setItems}>
{items.map((item) => (
<Reorder.Item key={item.id} value={item}>
{item.label}
</Reorder.Item>
))}
</Reorder.Group>
)
}
"use client"
import { motion } from "motion/react"
const OFFSET_THRESHOLD = 50
const VELOCITY_THRESHOLD = 300
<motion.div
drag="x"
dragConstraints={{ left: 0, right: 0 }}
onDragEnd={(_, info) => {
const swipedRight = info.offset.x > OFFSET_THRESHOLD || info.velocity.x > VELOCITY_THRESHOLD
const swipedLeft = info.offset.x < -OFFSET_THRESHOLD || info.velocity.x < -VELOCITY_THRESHOLD
if (swipedRight) onSwipeRight()
if (swipedLeft) onSwipeLeft()
}}
/>
import { useRef } from "react"
export function useLongPress(callback: () => void, ms = 600) {
const timerRef = useRef<ReturnType<typeof setTimeout>>()
return {
onPointerDown: () => { timerRef.current = setTimeout(callback, ms) },
onPointerUp: () => clearTimeout(timerRef.current),
onPointerLeave: () => clearTimeout(timerRef.current),
}
}
"use client"
import { motion } from "motion/react"
import { springs } from "@/lib/motion-tokens"
export function AnimatedText({ text }: { text: string }) {
return (
<motion.p
variants={{ visible: { transition: { staggerChildren: 0.05 } } }}
initial="hidden"
animate="visible"
>
{text.split(" ").map((word, i) => (
<motion.span
key={i}
className="inline-block mr-1"
variants={{
hidden: { opacity: 0, y: 12 },
visible: { opacity: 1, y: 0, transition: springs.gentle },
}}
>
{word}
</motion.span>
))}
</motion.p>
)
}
"use client"
import { useRef, useEffect } from "react"
import { animate } from "motion"
import { motionTokens } from "@/lib/motion-tokens"
export function Counter({ to }: { to: number }) {
const nodeRef = useRef<HTMLSpanElement>(null)
useEffect(() => {
const controls = animate(0, to, {
duration: motionTokens.duration.crawl,
ease: motionTokens.easing.smooth,
onUpdate: (v) => {
if (nodeRef.current) nodeRef.current.textContent = Math.round(v).toString()
},
})
return controls.stop // Rule 7: cleanup
}, [to])
return <span ref={nodeRef} />
}
"use client"
import { motion } from "motion/react"
import { motionTokens } from "@/lib/motion-tokens"
<motion.path
d="M 0 100 Q 50 0 100 100"
initial={{ pathLength: 0, opacity: 0 }}
animate={{ pathLength: 1, opacity: 1 }}
transition={{ duration: motionTokens.duration.slow, ease: motionTokens.easing.smooth }}
/>
"use client"
import { motion } from "motion/react"
import { motionTokens } from "@/lib/motion-tokens"
const CIRCUMFERENCE = 2 * Math.PI * 40 // r=40
export function ProgressRing({ progress }: { progress: number }) {
return (
<svg width="100" height="100" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="40" fill="none" stroke="#e5e7eb" strokeWidth="8" />
<motion.circle
cx="50" cy="50" r="40"
fill="none" stroke="#6366f1" strokeWidth="8"
strokeLinecap="round"
strokeDasharray={CIRCUMFERENCE}
animate={{ strokeDashoffset: CIRCUMFERENCE - (progress / 100) * CIRCUMFERENCE }}
transition={{ duration: motionTokens.duration.normal, ease: motionTokens.easing.smooth }}
style={{ rotate: -90, transformOrigin: "center" }}
/>
</svg>
)
}
"use client"
import { useRef } from "react"
import { useScroll, useTransform } from "motion/react"
import { motionTokens } from "@/lib/motion-tokens"
export function useScrollReveal() {
const ref = useRef(null)
const { scrollYProgress } = useScroll({ target: ref, offset: ["start end", "end start"] })
const opacity = useTransform(scrollYProgress, [0, 0.3], [0, 1])
const y = useTransform(scrollYProgress, [0, 0.3], [motionTokens.distance.lg, 0])
return { ref, style: { opacity, y } }
}
// Usage
const { ref, style } = useScrollReveal()
<motion.section ref={ref} style={style} />
"use client"
import { useEffect } from "react"
import { motion, useMotionValue, useSpring } from "motion/react"
import { springs } from "@/lib/motion-tokens"
export function CursorFollower() {
const x = useMotionValue(-100)
const y = useMotionValue(-100)
const sx = useSpring(x, springs.gentle)
const sy = useSpring(y, springs.gentle)
useEffect(() => {
const move = (e: MouseEvent) => { x.set(e.clientX); y.set(e.clientY) }
window.addEventListener("mousemove", move)
return () => window.removeEventListener("mousemove", move) // Rule 7
}, [])
return (
<motion.div
className="fixed top-0 left-0 w-6 h-6 rounded-full bg-indigo-500
pointer-events-none -translate-x-1/2 -translate-y-1/2 z-50"
style={{ x: sx, y: sy }}
/>
)
}
"use client"
import { useEffect } from "react"
import { motion, useAnimation } from "motion/react"
import { motionTokens } from "@/lib/motion-tokens"
export function ShimmerSkeleton({ className = "" }: { className?: string }) {
const controls = useAnimation()
useEffect(() => {
const play = () =>
controls.start({
x: ["-100%", "100%"],
transition: {
repeat: Infinity,
duration: motionTokens.duration.crawl,
ease: motionTokens.easing.linear,
},
})
const handleVisibility = () => {
if (document.visibilityState === "hidden") controls.stop()
else void play()
}
void play()
document.addEventListener("visibilitychange", handleVisibility)
return () => {
controls.stop()
document.removeEventListener("visibilitychange", handleVisibility)
}
}, [controls])
return (
<div className={`relative overflow-hidden bg-gray-200 rounded ${className}`}>
<motion.div
className="absolute inset-0 bg-gradient-to-r from-transparent via-white/60 to-transparent"
initial={{ x: "-100%" }}
animate={controls}
/>
</div>
)
}
"use client"
import { motion, AnimatePresence } from "motion/react"
import { motionTokens, springs } from "@/lib/motion-tokens"
export function LoadingButton({
loading,
label,
onClick,
}: {
loading: boolean
label: string
onClick: () => void
}) {
return (
<motion.button
onClick={onClick}
animate={{ opacity: loading ? 0.7 : 1 }}
whileTap={loading ? {} : { scale: motionTokens.scale.press }}
transition={springs.snappy}
disabled={loading}
>
<AnimatePresence mode="wait">
{loading ? (
<motion.span
key="loading"
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
transition={{ duration: motionTokens.duration.fast }}
>
…
</motion.span>
) : (
<motion.span
key="label"
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
transition={{ duration: motionTokens.duration.fast }}
>
{label}
</motion.span>
)}
</AnimatePresence>
</motion.button>
)
}
"use client"
import { useEffect } from "react"
import { motion, useAnimation } from "motion/react"
import { motionTokens } from "@/lib/motion-tokens"
export function PulseDot() {
const controls = useAnimation()
useEffect(() => {
const pulse = () =>
controls.start({
scale: [1, 1.4, 1],
opacity: [1, 0.6, 1],
transition: { repeat: Infinity, duration: motionTokens.duration.crawl },
})
// Rule 2: pause when tab is hidden
const handleVisibility = () => {
if (document.visibilityState === "hidden") controls.stop()
else void pulse()
}
void pulse()
document.addEventListener("visibilitychange", handleVisibility)
// Rule 7: stop controls and remove listeners on unmount.
return () => {
controls.stop()
document.removeEventListener("visibilitychange", handleVisibility)
}
}, [controls])
return <motion.span className="w-2 h-2 rounded-full bg-green-400" animate={controls} />
}
Drag-to-dismiss sheet with shimmer content, loading state, and reduced motion
support — combining useMotionValue, useTransform, useSafeMotion,
AnimatePresence, and tokens from motion-foundations:
"use client"
import { useState } from "react"
import { motion, AnimatePresence, useMotionValue, useTransform } from "motion/react"
import { springs, motionTokens } from "@/lib/motion-tokens"
import { useSafeMotion } from "@/hooks/use-reduced-motion"
import { ShimmerSkeleton } from "./shimmer-skeleton"
export function DismissibleSheet({
isOpen,
onClose,
loading,
children,
}: {
isOpen: boolean
onClose: () => void
loading: boolean
children: React.ReactNode
}) {
const safe = useSafeMotion(motionTokens.distance.xl)
const y = useMotionValue(0)
const opacity = useTransform(y, [0, 200], [1, 0])
return (
<AnimatePresence>
{isOpen && (
<>
{/* Backdrop */}
<motion.div
key="backdrop"
className="fixed inset-0 bg-black/40"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
/>
{/* Sheet — drag-to-dismiss */}
<motion.div
key="sheet"
className="fixed bottom-0 inset-x-0 rounded-t-2xl bg-white p-6"
drag="y"
dragConstraints={{ top: 0 }}
style={{ y, opacity }}
onDragEnd={(_, info) => {
if (info.offset.y > 120 || info.velocity.y > 500) onClose()
}}
initial={safe.initial}
animate={safe.animate}
exit={safe.exit}
transition={springs.gentle}
>
{loading ? (
<div className="space-y-3">
<ShimmerSkeleton className="h-4 w-3/4" />
<ShimmerSkeleton className="h-4 w-1/2" />
<ShimmerSkeleton className="h-20 w-full" />
</div>
) : children}
</motion.div>
</>
)}
</AnimatePresence>
)
}
This skill does not cover:
motion-foundationsmotion-patternsanimate-* without motion/react| Anti-pattern | Rule violated | Fix |
|---|---|---|
drag tested only on desktop | Rule 1 | Test on touch emulator and real device |
animate={{ repeat: Infinity }} with no pause | Rule 2 | Add visibilitychange listener |
onDragEnd checking only offset, not velocity | Rule 3 | Check both info.offset and info.velocity |
animate(scope, ...) before useEffect | Rule 4 | Call animate() only after mount |
const x = new MotionValue(0) in render | Rule 5 | Use const x = useMotionValue(0) |
transition={{ duration: 1.2 }} inline | Rule 6 | Use motionTokens.duration.crawl |
useEffect without cleanup | Rule 7 | Return removeEventListener / controls.stop |
| SVG morph between paths with different commands | Rule 8 | Normalize path commands before animating |
motion-foundations — defines all tokens, springs, useSafeMotion, and SSR guards imported here. Must be set up before using this skill.motion-patterns — handles standard UI patterns (button, modal, stagger, page transitions, scroll reveals). Use it before reaching for the advanced patterns here.