"use client"; import { useEffect, useRef, useState, type ReactNode } from "react"; import { cn } from "@/lib/utils"; /** * 通用入场动画包装器(渐进增强)。 * * 与 MotionItem 采用同一策略:服务端输出的 HTML 默认可见, * 仅在客户端确认元素位于视口下方时,才叠加一次淡入上移动画。 * * 这样即使 JS 完全不可用,内容依然完整可读——动画纯粹是增强项。 */ export interface RevealProps { children: ReactNode; className?: string; /** 延迟秒数,用于列表的错位入场 */ delay?: number; /** 位移距离(Tailwind 间距类,如 "translate-y-3") */ offsetClassName?: string; } export function Reveal({ children, className, delay = 0, offsetClassName = "translate-y-4", }: RevealProps) { const ref = useRef(null); const [animationClass, setAnimationClass] = useState(""); useEffect(() => { const element = ref.current; if (!element) { return; } const prefersReducedMotion = window.matchMedia( "(prefers-reduced-motion: reduce)", ).matches; if (prefersReducedMotion) { return; } // 首屏内可见的元素不做动画,避免内容闪烁 const rect = element.getBoundingClientRect(); if (rect.top <= window.innerHeight) { return; } setAnimationClass(cn("opacity-0", offsetClassName)); const frame = requestAnimationFrame(() => { requestAnimationFrame(() => setAnimationClass("")); }); return () => cancelAnimationFrame(frame); }, [offsetClassName]); return (
0 ? { transitionDelay: `${delay}s` } : undefined} > {children}
); }