Communitygithub.com

Motion Advanced

高度なアニメーション(ドラッグ&ドロップ、ジェスチャー、テキストアニメーション、SVGパス描画、カスタムフック、useAnimateによる命令的シーケンス)をReact/Next.jsで実現します。 motion-foundations のセットアップが必須です。

Motion Advanced とは?

このスキルは、React および Next.js アプリケーションにおいて、複雑でインタラクティブ、かつ物理ベースのアニメーションパターンを実装するための包括的なガイドです。ドラッグ&ドロップ、スワイプ、ロングプレスなどの高度なジェスチャー、テキストの単語・文字単位でのアニメーション、SVGパスの描画やアイコンのモーフィング、カスタムフック (useScrollReveal, カーソルフォロワー)、useAnimate を使った命令的アニメーションシーケンス、ローダーやスケルトンUIまでをカバーします。 motion-patterns でカバーできない高度なユースケース向けに設計されており、 motion-foundations のトークンと組み合わせて使用します。物理ベースのアニメーション、タッチデバイスでのテスト、バックグラウンドタブでのアニメーション停止、適切なクリーンアップといった重要な原則とルールも詳細に定められており、プロダクション品質のインタラクションを構築できます。

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

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

Motion Advanced は何をしますか?

複雑でインタラクティブ、かつ物理ベースのアニメーションパターン。 事前に motion-foundations のセットアップが必要です。 motion-patterns では不十分な場合に使用してください。

アクティブにするタイミング

  • ドラッグ・トゥ・ディスミスシート、スワイプジェスチャー、並べ替え可能なリストを構築する場合
  • テキストを単語単位、文字単位でアニメーションさせたり、ライブカウンターとして表示する場合
  • SVGパスを描画したり、アイコンをモーフィングしたり、円形プログレスをアニメーションさせる場合
  • カスタムアニメーションフック(useScrollReveal、マグネティックボタン、カーソルフォロワー)を作成する場合
  • useAnimate を使用してマルチステップアニメーションを命令的にシーケンスする場合
  • スピナー、シャイマースケルトン、パルスインジケーター、ローディングボタン状態を構築する場合

出力

このスキルは以下を生成します:

  • ドラッグインタラクション: ドラッグ可能なカード、ドラッグ・トゥ・ディスミスシート、Reorder.Group リスト
  • ジェスチャーフック: スワイプ検出、ロングプレス、ピンチアウトライン
  • テキストアニメーションコンポーネント: 単語リビール、キャラクタータイプライター、数字カウンター
  • SVGアニメーション: パス描画、アイコンモーフ、ストロークプログレスリング
  • カスタムフック: useScrollReveal、useHoverScale、useNavigationDirection、useInViewOnce
  • useAnimate による命令的シーケンス(割り込み安全な async/await)
  • ローダーコンポーネント: スピナー、シャイマー、パルスドット、プログレスバー、ボタンローディング状態

原則

  • 物理ベースのモーション(useSpring、springs.*)は、直接操作において常に時間ベースよりも自然に感じられます。
  • useMotionValue + useTransform は、再レンダリングをトリガーせずに派生値を計算します。
  • useAnimate シーケンスは命令的で割り込み安全です – animate() を途中で呼び出すと、前のアニメーションが自動的にキャンセルされます。
  • モーション値(useMotionValue、useSpring)はSSR安全であり、ハイドレーションエラーを引き起こしません。

ルール

  1. ドラッグインタラクションは、マウスだけでなくタッチデバイスでもテストする必要があります。 drag プロパティは両方で動作しますが、フィーリングとしきい値は異なります。
  2. 無限アニメーションは、document.visibilityState === "hidden" のときに一時停止しなければなりません。 バックグラウンドタブはGPU/CPUを消費してはいけません。
  3. スワイプしきい値は明示的に指定する必要があります。 速度だけで意図を推測してはいけません。offset + velocity チェックを組み合わせてください。
  4. useAnimate のスコープrefは、マウントされたDOM要素にアタッチする必要があります。 マウント前に animate() を呼び出すと、サイレントエラーが発生します。
  5. モーション値はレンダリングごとに再生成してはいけません。 コンポーネント本体内で useMotionValue(0) を使用するのは正しいですが、レンダー内で new MotionValue(0) を使用するのは誤りです。
  6. すべてのトークン値は motion-foundations からインポートされます。 インライン数値は使用しないでください。
  7. カスタムフックはクリーンアップを処理する必要があります。 すべての window.addEventListener には、useEffect の戻り値に対応する removeEventListener が必要です。
  8. SVGモーフィングには、パスコマンド数が等しい必要があります。 コマンド構造が異なるパスは、補間されずにスナップします。

決定ガイダンス

適切な高度なAPIの選択

シナリオAPI
リリース時に物理ベースのドラッグdrag + dragTransition: springs.release
順序付けられたドラッグ・トゥ・リオーダーリストReorder.Group + Reorder.Item
ドラッグオフセットで解除drag="y" + onDragEnd オフセットチェック
左右スワイプdrag="x" + onDragEnd オフセットチェック
ロングプレスuseLongPress フック
時間経過で平滑化された値useSpring
別の値から派生した値useTransform
マルチステップシーケンスuseAnimate と async/await
ワンショット命令的アニメーションmotion からの animate()
単語ずつ表示されるテキストinline-block スパンのスタガー
SVG描画pathLength 0 → 1
SVGモーフd 属性トゥイーン(等しいコマンド)
円形プログレスstrokeDashoffset トゥイーン

useSpring とスプリングトランジションの使い分け

useSpringtransition: springs.*
使用場面カーソルフォロワー、ポインター追跡値離散的な状態変化
更新連続的、毎フレーム状態変化によってトリガー
割り込みスムーズ – 物理が速度から引き継ぐ現在の値から再開

コアコンセプト

useMotionValue + useTransform

再レンダリングなしのリアクティブ計算:

const x = useMotionValue(0)
const opacity = useTransform(x, [-200, 0, 200], [0, 1, 0])
// opacity は x が変化するたびに毎フレーム更新 – setState不要、再レンダリング不要

useAnimate

[scope, animate] を返します。スコープrefはDOM要素にアタッチする必要があります。 animate() 呼び出しは割り込み安全です – 途中で呼び出すと前の実行がキャンセルされます。

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) => {
        // ルール3: 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   // ルール7: クリーンアップ
  }, [to])

  return <span ref={nodeRef} />
}

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

ストロークプログレスリング

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

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

// 使用法
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)   // ルール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 },
      })

    // ルール2: タブが非表示のときに一時停止
    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.

関連スキル