whileInView · amount 0.4 · 500ms

Scroll Reveal

スクロールでビューポートに入るたびカードが fade + slide インする。once: false なので何度でも再発火する。

whileInView · amount 0.4 · 500ms

Insight

scroll で発火する viewport reveal

Pattern

whileInView で1回 or 何度でも

Stagger

viewport amount を調整

Performance

GPU layer に乗せる

A11y

prefers-reduced-motion 尊重

Best

30% 表示で発火が標準

/**
 * Scroll Reveal — whileInView で viewport 進入/退出のたびに再発火するカード。
 * 移植元: motion-playground app/_sections/ScrollReveal.tsx(dark 変換 M-B-2)
 * stage 内スクロールで表示されるたび発火するため useLabReplay は不要。
 */
import { motion } from 'motion/react';
import { easings } from '../../../lib/motion-tokens/playground';

const cards = [
  { title: 'Insight', body: 'scroll で発火する viewport reveal' },
  { title: 'Pattern', body: 'whileInView で1回 or 何度でも' },
  { title: 'Stagger', body: 'viewport amount を調整' },
  { title: 'Performance', body: 'GPU layer に乗せる' },
  { title: 'A11y', body: 'prefers-reduced-motion 尊重' },
  { title: 'Best', body: '30% 表示で発火が標準' },
];

export default function ScrollReveal() {
  return (
    <div
      data-lab-island
      className="grid w-full gap-4 p-4 sm:grid-cols-2 lg:p-6"
    >
      {cards.map((card, i) => (
        <motion.div
          key={card.title}
          initial={{ opacity: 0, y: 32 }}
          whileInView={{ opacity: 1, y: 0 }}
          viewport={{ once: false, amount: 0.4 }}
          transition={{
            duration: 0.5,
            ease: easings.md3Emphasized,
            delay: (i % 2) * 0.08,
          }}
          className="rounded-xl border border-[var(--border-subtle)] bg-[var(--bg-surface)] p-5"
        >
          <p className="text-sm font-medium text-[var(--text-primary)]">{card.title}</p>
          <p className="mt-1 text-xs text-[var(--text-muted)]">{card.body}</p>
        </motion.div>
      ))}
    </div>
  );
}