Communitygithub.com

motion-advanced

Patrones de animación avanzados para React/Next.js — arrastrar y soltar, gestos, animaciones de texto, dibujo de rutas SVG, hooks personalizados, secuencias imperativas (useAnimate), loaders y el árbol de decisión completo de la API. Requiere motion-foundations. Úsalo al construir arrastrar y soltar, gestos, animaciones de texto o SVG, o secuencias de animación imperativas en React o Next.js.

¿Qué es motion-advanced?

Motion Advanced es una skill diseñada para desarrolladores que trabajan con React y Next.js y necesitan implementar patrones de animación complejos e interactivos más allá de las capacidades básicas de motion-patterns. Esta skill proporciona una guía completa para construir interacciones como arrastrar para descartar (drag-to-dismiss), gestos de deslizamiento, listas reordenables, animaciones de texto palabra por palabra o carácter por carácter, dibujo de rutas SVG, morphing de iconos, indicadores de progreso circulares, hooks personalizados (como useScrollReveal o useLongPress), secuencias imperativas con useAnimate y componentes de carga como spinners, shimmers y estados de botón de carga. La documentación incluye reglas fundamentales, como la necesidad de probar interacciones de arrastre en dispositivos táctiles, pausar animaciones infinitas cuando la pestaña está oculta, usar umbrales explícitos para gestos, limpiar event listeners en hooks personalizados, y mantener el mismo número de comandos en rutas SVG para morphing. También ofrece una tabla de decisión para elegir la API correcta según el escenario, y explica conceptos clave como useMotionValue combinado con useTransform para cómputos reactivos sin re-renders, y useAnimate para secuencias imperativas seguras ante interrupciones. La skill se apoya en motion-foundations para todos los valores de tokens, evitando números inline, y es adecuada para aplicaciones que requieren animaciones fluidas y basadas en física.

Compatible con~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/affaan-m/ECC/tree/main/skills/motion-advanced

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

Motion Avanzado

Patrones de animación complejos, interactivos y basados en física. Requiere tener configurado motion-foundations primero. Úsalos cuando motion-patterns no sea suficiente.

Cuándo activarlo

  • Construir hojas deslizables para descartar, gestos de deslizamiento o listas reordenables
  • Animar texto palabra por palabra, carácter por carácter o como un contador en vivo
  • Dibujar rutas SVG, transformar iconos o animar progreso circular
  • Escribir un hook de animación personalizado (useScrollReveal, botón magnético, seguidor de cursor)
  • Secuenciar animaciones de varios pasos de manera imperativa con useAnimate
  • Construir spinners, esqueletos shimmer, indicadores de pulso o estados de botón de carga

Resultados

Esta skill produce:

  • Interacciones de arrastre: tarjetas arrastrables, hojas deslizables para descartar, listas Reorder.Group
  • Hooks de gestos: detección de deslizamiento, presión larga, contorno de pellizco
  • Componentes de animación de texto: revelación de palabras, máquina de escribir de caracteres, contador numérico
  • Animación SVG: dibujo de rutas, transformación de iconos, anillo de progreso de trazo
  • Hooks personalizados: useScrollReveal, useHoverScale, useNavigationDirection, useInViewOnce
  • Secuencias imperativas mediante useAnimate con async/await seguro ante interrupciones
  • Componentes de carga: spinner, shimmer, punto de pulso, barra de progreso, estado de carga de botón

Principios

  • El movimiento basado en física (useSpring, springs.*) siempre se siente más natural que el basado en duración para manipulación directa.
  • useMotionValue + useTransform calcula valores derivados sin provocar re-renderizados.
  • Las secuencias de useAnimate son imperativas y seguras ante interrupciones: llamar a animate() en medio del vuelo cancela automáticamente la animación anterior.
  • Los valores de movimiento (useMotionValue, useSpring) son seguros para SSR y no causan errores de hidratación.

Reglas

  1. Las interacciones de arrastre deben probarse en dispositivos táctiles, no solo con el ratón. La propiedad drag funciona en ambos, pero la sensación y el umbral difieren.
  2. Las animaciones infinitas deben pausarse cuando document.visibilityState === "hidden". Las pestañas en segundo plano no deben consumir GPU/CPU.
  3. El umbral de deslizamiento debe ser explícito. Nunca inferir la intención solo a partir de la velocidad; combinar comprobaciones de offset + velocity.
  4. El ref de ámbito de useAnimate debe estar adjunto a un elemento DOM montado. Llamar a animate() antes del montaje lanza un error silencioso.
  5. Los valores de movimiento no deben recrearse en cada renderizado. useMotionValue(0) dentro del cuerpo de un componente es correcto; new MotionValue(0) en un renderizado no lo es.
  6. Todos los valores de token se importan de motion-foundations. Sin números inline.
  7. Los hooks personalizados deben manejar la limpieza. Cada window.addEventListener necesita un removeEventListener correspondiente en el retorno del useEffect.
  8. La transformación de SVG requiere igual número de comandos de ruta. Las rutas con diferentes estructuras de comandos saltan en lugar de interpolarse.

Guía de decisión

Elegir la API avanzada correcta

EscenarioAPI
Arrastre con física al soltardrag + dragTransition: springs.release
Lista ordenada arrastrableReorder.Group + Reorder.Item
Descartar al arrastrardrag="y" + comprobación de offset en onDragEnd
Deslizar izquierda/derechadrag="x" + comprobación de offset en onDragEnd
Presión largaHook useLongPress
Valor suavizado en el tiempouseSpring
Valor derivado de otrouseTransform
Secuencia de varios pasosuseAnimate con async/await
Animación imperativa únicaanimate() de motion
Texto que aparece palabra por palabraEscalonamiento en spans inline-block
Dibujo de SVGpathLength 0 → 1
Transformación de SVGInterpolación del atributo d (mismos comandos)
Progreso circularInterpolación de strokeDashoffset

Cuándo usar useSpring frente a una transición spring

useSpringtransition: springs.*
Úsalo paraSeguidor de cursor, valores rastreados por punteroCambios de estado discretos
ActualizacionesContinuas, en cada fotogramaActivadas por cambio de estado
InterrupciónSuave: la física retoma desde la velocidadSe reinicia desde el valor actual

Conceptos clave

useMotionValue + useTransform

Cómputo reactivo sin re-renderizados:

const x = useMotionValue(0)
const opacity = useTransform(x, [-200, 0, 200], [0, 1, 0])
// la opacidad se actualiza en cada fotograma a medida que x cambia — sin setState, sin re-renderizado

useAnimate

Devuelve [scope, animate]. El ref de ámbito debe estar adjunto a un elemento DOM. Las llamadas a animate() son seguras ante interrupciones: llamar en medio del vuelo cancela la ejecución anterior.

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 })  // dispara y olvida
}

return <div ref={scope}>...</div>

Ejemplos de código

Tarjeta arrastrable

"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}
/>

Hoja deslizable para descartar

"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) => {
        // Regla 3: combinar offset + velocity
        if (info.offset.y > 120 || info.velocity.y > 500) onClose()
      }}
    />
  )
}

Lista reordenable

"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>
  )
}

Detección de deslizamiento

"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()
  }}
/>

Hook de presión larga

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),
  }
}

Revelación palabra por palabra

"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>
  )
}

Contador numérico

"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   // Regla 7: limpieza
  }, [to])

  return <span ref={nodeRef} />
}

Dibujo de ruta SVG

"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 }}
/>

Anillo de progreso de trazo

"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>
  )
}

Hook useScrollReveal

"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 } }
}

// Uso
const { ref, style } = useScrollReveal()
<motion.section ref={ref} style={style} />

Seguidor de cursor

"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)   // Regla 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 }}
    />
  )
}

Esqueleto shimmer

"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>
  )
}

Estado de carga de botón

"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>
  )
}

Animación infinita con pausa por visibilidad

"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 },
      })

    // Regla 2: pausar cuando la pestaña está oculta
    const handleVisibility = () => {
      if (document.visibilityState === "hidden") controls.stop()
      else void pulse()
    }

    void pulse()
    document.addEventListener("visibilitychange", handleVisibility)
    return () => {
      controls.stop()
      document.removeEventListener("visibilitychange", handleVisibility)
    }
  }, [controls])

  return (
    <motion.span
      className="inline-block w-3 h-3 rounded-full bg-indigo-500"
      animate={controls}
    />
  )
}

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/content-engine

Create platform-native content systems for X, LinkedIn, TikTok, YouTube, newsletters, and repurposed multi-platform campaigns. Use when the user wants social posts, threads, scripts, content calendars, or one source asset adapted cleanly across platforms.

affaan-m/ECC

Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.

affaan-m/fal-ai-media

Unified media generation via fal.ai MCP — image, video, and audio. Covers text-to-image (Nano Banana), text/image-to-video (Seedance, Kling, Veo 3), text-to-speech (CSM-1B), and video-to-audio (ThinkSound). Use when the user wants to generate images, videos, or audio with AI.

affaan-m/manim-video

日本語翻訳:このファイルは manim-video 用の日本語翻訳が必要です

affaan-m/remotion-video-creation

Remotion のベストプラクティス - React で動画を作成する。3D、アニメーション、音声、字幕、チャート、トランジションなどをカバーするドメイン固有の29のルール。

affaan-m/video-editing

AI-assisted video editing workflows for cutting, structuring, and augmenting real footage. Covers the full pipeline from raw capture through FFmpeg, Remotion, ElevenLabs, fal.ai, and final polish in Descript or CapCut. Use when the user wants to edit video, cut footage, create vlogs, or build video content.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

agent-self-evaluation

Use after completing any non-trivial task. The agent self-rates its output on 5 axes — accuracy, completeness, clarity, actionability, conciseness — with concrete evidence per criterion. Produces a structured 1-5 scorecard with specific improvement suggestions.

agent-sort

Build an evidence-backed ECC install plan for a specific repo by sorting skills, commands, rules, hooks, and extras into DAILY vs LIBRARY buckets using parallel repo-aware review passes. Use when ECC should be trimmed to what a project actually needs instead of loading the full bundle.

ai-first-engineering

Engineering operating model for teams where AI agents generate a large share of implementation output. Use when setting team process, review gates, or ownership rules for a codebase largely written by agents.

ai-regression-testing

Regression testing strategies for AI-assisted development. Sandbox-mode API testing without database dependencies, automated bug-check workflows, and patterns to catch AI blind spots where the same model writes and reviews code. Use when adding regression coverage to AI-assisted code, or when the same model both wrote and reviewed a change.

android-clean-architecture

Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories, and data layer patterns. Use when structuring modules, layers, or data flow in an Android or KMP project.

angular-developer

Generates Angular code and provides architectural guidance. Trigger when creating projects, components, or services, or for best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, or CLI tooling.

Skills relacionados