Files
Godopu f3972cac27 feat(animation): implement subtle and high-performance scroll reveal animations across public pages
- Enhance Reveal component with HTMLAttributes passthrough and inline style merging
- Integrate Reveal into Home, Members, Publications, Lectures, and Standardization sections
- Apply lightweight stagger delay (max 360ms cap) without breaking grid stretch (h-full)
- Preserve ProjectPageView CSS Scroll Snap direct-child contract via as='article' in-place replacement
- Integrate Counter component for metrics and category badges
- Support prefers-reduced-motion media query and noscript fallback
- All 12 quality gates passed clean with unanimous PASS from all reviewers
2026-08-26 00:11:24 +09:00

68 lines
1.5 KiB
TypeScript

"use client";
import { useEffect, useRef, type ElementType, type ReactNode, type HTMLAttributes } from "react";
type Variant = "up" | "left" | "right" | "scale";
/**
* Scroll-triggered reveal. Uses a single IntersectionObserver (no deps).
* Pairs with the `.reveal` / `.is-visible` rules in globals.css.
*
* CUSTOMIZATION HOOK — MOTION:
* variant → direction of entrance
* delay → stagger (ms), applied as transition-delay
*/
export default function Reveal({
children,
variant = "up",
delay = 0,
as: Tag = "div",
className = "",
style,
...rest
}: HTMLAttributes<HTMLElement> & {
children: ReactNode;
variant?: Variant;
delay?: number;
as?: ElementType;
className?: string;
}) {
const ref = useRef<HTMLElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const io = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("is-visible");
io.unobserve(entry.target);
}
});
},
{ threshold: 0.12, rootMargin: "0px 0px -8% 0px" }
);
io.observe(el);
return () => io.disconnect();
}, []);
const combinedStyle = delay
? { ...style, transitionDelay: `${delay}ms` }
: style;
return (
<Tag
ref={ref}
data-variant={variant}
style={combinedStyle}
className={`reveal ${className}`}
{...rest}
>
{children}
</Tag>
);
}