agency-web/app/components/KineticHeadline.tsx

64 lines
1.6 KiB
TypeScript

"use client";
import { useEffect, useRef } from "react";
import { gsap } from "gsap";
import SplitType from "split-type";
/**
* Splits the hero H1 into lines + chars and reveals them with a staggered
* mask-up on load — the orchestrated "page load" delight moment.
* Under reduced motion the text is simply shown as-is. The real text is
* always in the DOM (SplitType only re-wraps the same characters), so SEO
* and screen readers read the full heading.
*/
export default function KineticHeadline({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) {
const ref = useRef<HTMLHeadingElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduce) {
el.style.opacity = "1";
return;
}
const split = new SplitType(el, { types: "lines,words" });
el.style.opacity = "1";
// wrap each line in an overflow-hidden mask
split.lines?.forEach((line) => {
const mask = document.createElement("span");
mask.className = "line-mask";
line.parentNode?.insertBefore(mask, line);
mask.appendChild(line);
});
const tween = gsap.from(split.words || [], {
yPercent: 115,
opacity: 0,
rotateZ: 2,
duration: 1,
ease: "expo.out",
stagger: 0.04,
delay: 0.15,
});
return () => {
tween.kill();
split.revert();
};
}, []);
return (
<h1 ref={ref} className={className} style={{ opacity: 0 }}>
{children}
</h1>
);
}