Files
app-261004/components/motion/reveal.tsx
T
2026-10-08 11:09:37 +08:00

74 lines
1.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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<HTMLDivElement>(null);
const [animationClass, setAnimationClass] = useState<string>("");
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 (
<div
ref={ref}
className={cn(
"motion-safe:transition-[opacity,transform] motion-safe:duration-500 motion-safe:ease-brand",
animationClass,
className,
)}
style={delay > 0 ? { transitionDelay: `${delay}s` } : undefined}
>
{children}
</div>
);
}