Communitygithub.com

motion-advanced

React / Next.js向け高度なモーションパターン — ドラッグ&ドロップ、ジェスチャー、テキストアニメーション、SVGパス描画、カスタムフック、命令型シーケンス(useAnimate)、ローダー、API決定木。motion-foundationsが必要です。ドラッグ&ドロップ、ジェスチャー、テキストやSVGアニメーション、ReactやNext.jsでの命令型アニメーションシーケンスを構築する際に使用します。

motion-advanced とは?

このスキルは、ReactおよびNext.jsアプリケーションにおける高度なアニメーションパターンを提供します。ドラッグ&ドロップ、スワイプジェスチャー、テキストアニメーション、SVGパス描画、カスタムフック、命令型アニメーションシーケンスなど、複雑でインタラクティブなアニメーションを実装するためのものです。`motion-foundations`を前提としており、基本的な`motion-patterns`では不十分な場合に有効です。使用するAPIの選択肢を明確にする決定木(Decision Guidance)と、実装のベストプラクティスをまとめたルール(Rules)が含まれています。具体的なコード例として、ドラッグ可能なカード、ドラッグで閉じるシート、並べ替え可能なリスト、単語ごとに表示するテキストアニメーション、SVGパスの描画アニメーション、進行状況リング、スクロール連動表示、カーソルフォロワー、ローディング状態のボタンなどが提供されています。

対応~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/affaan-m/everything-claude-code/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 のスコープ参照は、マウントされた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 と spring transition の使い分け

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

コアコンセプト

useMotionValue + useTransform

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

const x = useMotionValue(0)
const opacity = useTransform(x, [-200, 0, 200], [0, 1, 0])
// xが変化するたびにopacityが更新される — setStateなし、再レンダリングなし

useAnimate

[scope, animate] を返します。スコープ参照は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 })  // ファイア&フォーゲット
}

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.div animate={controls} className="w-3 h-3 rounded-full bg-indigo-500" />
}

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/claude-api

Anthropic Claude API patterns for Python and TypeScript. Covers Messages API, streaming, tool use, vision, extended thinking, batches, prompt caching, and Claude Agent SDK. Use when building applications with the Claude API or Anthropic SDKs.

affaan-m/everything-claude-code

End-to-end marketing campaign planning and execution. Covers audience research, positioning, campaign angle definition, landing page copy, email sequences, social posts, ad copy, short-form video scripts, and content calendars. Use as the orchestration layer for multi-channel product launches. Use when planning or executing a multi-channel product launch, or producing landing page, email, social, or ad copy.

affaan-m/everything-claude-code

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/everything-claude-code-conventions

Development conventions and patterns for everything-claude-code. JavaScript project with conventional commits.

affaan-m/frontend-design

Create distinctive, production-grade frontend interfaces with high design quality. Use when the user asks to build web components, pages, or applications and the visual direction matters as much as the code quality.

affaan-m/gget

gget CLI and Python workflow for quick genomic database queries, sequence lookup, BLAST-style searches, enrichment checks, and reproducible bioinformatics evidence logs.

affaan-m/literature-review

Systematic literature-review workflow for academic, biomedical, technical, and scientific topics, including search planning, source screening, synthesis, citation checks, and evidence logging.

affaan-m/motion-ui

Production-ready UI motion system for React/Next.js. Use when implementing animations, transitions, or motion patterns.

affaan-m/project-guidelines-example

Example project-specific skill template based on a real production application.

affaan-m/pubmed-database

Direct PubMed and NCBI E-utilities search workflows for biomedical literature, MeSH queries, PMID lookup, citation retrieval, and API-backed literature monitoring.

affaan-m/scholar-evaluation

Structured scholarly-work evaluation for papers, proposals, literature reviews, methods sections, evidence quality, citation support, and research-writing feedback.

affaan-m/uspto-database

USPTO patent and trademark data workflow for official record lookup, PatentSearch queries, TSDR checks, assignment data, and reproducible IP research logs.

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.

関連スキル