agency-web/app/components/KineticText.tsx

83 lines
2.3 KiB
TypeScript

"use client";
/**
* KineticText — word/line masked reveal driven by ScrollTrigger.
*
* Splits the given text into words wrapped in overflow-hidden masks (manual
* spans — no premium SplitText plugin) and slides each word up from below as it
* enters the viewport, with a stagger. Renders the text as real, selectable DOM
* so it stays accessible and SEO-safe; animation only transforms (perf-friendly).
*
* With reduced-motion, the text simply appears (no transform).
*/
import { createElement, useEffect, useRef, type ElementType } from "react";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger);
type Props = {
text: string;
as?: ElementType;
className?: string;
/** Delay the whole reveal (s). */
delay?: number;
/** Start animation on mount instead of on scroll (for the hero). */
immediate?: boolean;
/** Mark a word index range as gradient-highlighted. */
highlight?: [number, number];
};
export default function KineticText({
text,
as: Tag = "span",
className = "",
delay = 0,
immediate = false,
highlight,
}: Props) {
const ref = useRef<HTMLElement>(null);
const words = text.split(" ");
useEffect(() => {
const el = ref.current;
if (!el) return;
const inner = gsap.utils.toArray<HTMLElement>(".ktext__in", el);
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
gsap.set(inner, { yPercent: 0, opacity: 1 });
return;
}
const ctx = gsap.context(() => {
gsap.set(inner, { yPercent: 118 });
const anim: gsap.TweenVars = {
yPercent: 0,
duration: 1.0,
ease: "power4.out",
stagger: 0.045,
delay,
};
if (!immediate) {
anim.scrollTrigger = { trigger: el, start: "top 88%" };
}
gsap.to(inner, anim);
}, el);
return () => ctx.revert();
}, [delay, immediate]);
const children = words.map((w, i) => {
const hot =
highlight && i >= highlight[0] && i <= highlight[1] ? " ktext__word--grad" : "";
return (
<span className={`ktext__word${hot}`} key={i}>
<span className="ktext__in">{w}</span>
{i < words.length - 1 ? " " : ""}
</span>
);
});
return createElement(Tag, { ref, className: `ktext ${className}` }, children);
}