Files
website/src/components/home/Reveal.tsx
T

62 lines
1.5 KiB
TypeScript

"use client";
import { useEffect, useRef, useState, type ReactNode } from "react";
type RevealProps = {
children: ReactNode;
className?: string;
/** Stagger delay in ms once visible */
delay?: number;
/** Optional custom y offset in px */
offset?: number;
};
export default function Reveal({
children,
className = "",
delay = 0,
offset = 36,
}: RevealProps) {
const ref = useRef<HTMLDivElement>(null);
const [visible, setVisible] = useState(false);
useEffect(() => {
const el = ref.current;
if (!el) return;
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduce) {
setVisible(true);
return;
}
const io = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setVisible(true);
io.disconnect();
}
},
{ threshold: 0.12, rootMargin: "0px 0px -6% 0px" },
);
io.observe(el);
return () => io.disconnect();
}, []);
return (
<div
ref={ref}
className={className}
style={{
opacity: visible ? 1 : 0,
transform: visible ? "translate3d(0,0,0)" : `translate3d(0,${offset}px,0)`,
filter: visible ? "blur(0)" : "blur(8px)",
transition: `opacity 0.9s cubic-bezier(0.16,1,0.3,1) ${delay}ms, transform 0.9s cubic-bezier(0.16,1,0.3,1) ${delay}ms, filter 0.9s cubic-bezier(0.16,1,0.3,1) ${delay}ms`,
willChange: "opacity, transform, filter",
}}
>
{children}
</div>
);
}