"use client" import { useEffect, useRef } from "react" import { cn } from "@/lib/utils" import { onIntroEnd } from "@/modules/shared/lib/intro" // Resting state: heavy and condensed. Under the pointer: thin and wide. const REST = { wght: 800, wdth: 75 } const NEAR = { wght: 200, wdth: 125 } const RADIUS = 280 const EASE = 0.14 /** * Headline whose letters react to the pointer along the font's weight and * width axes. Each letter also slides in after the preloader (CSS). */ export function KineticHeadline({ lines, className, }: { lines: string[] className?: string }) { const rootRef = useRef(null) useEffect(() => { const root = rootRef.current if (!root) return const fine = window.matchMedia("(hover: hover) and (pointer: fine)").matches const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches if (!fine || reduced) return const chars = Array.from(root.querySelectorAll("[data-char]")) const current = chars.map(() => 0) let pointerX = -9999 let pointerY = -9999 let frame = 0 let active = false const tick = () => { // Read all positions first, then write, to avoid layout thrashing const rects = chars.map((char) => char.getBoundingClientRect()) let moving = false rects.forEach((rect, index) => { const dx = pointerX - (rect.left + rect.width / 2) const dy = pointerY - (rect.top + rect.height / 2) const distance = Math.hypot(dx, dy) const raw = Math.max(0, 1 - distance / RADIUS) const target = raw * raw * (3 - 2 * raw) current[index] += (target - current[index]) * EASE if (Math.abs(target - current[index]) > 0.002) moving = true }) chars.forEach((char, index) => { const t = current[index] const wght = REST.wght + (NEAR.wght - REST.wght) * t const wdth = REST.wdth + (NEAR.wdth - REST.wdth) * t char.style.fontVariationSettings = `"wght" ${wght.toFixed(0)}, "wdth" ${wdth.toFixed(1)}` }) frame = moving ? requestAnimationFrame(tick) : 0 } const onMove = (event: PointerEvent) => { if (!active) return pointerX = event.clientX pointerY = event.clientY if (!frame) frame = requestAnimationFrame(tick) } const onLeave = () => { pointerX = pointerY = -9999 if (!frame) frame = requestAnimationFrame(tick) } // Wait for the letters to finish sliding in let timeout = 0 const cleanupIntro = onIntroEnd(() => { timeout = window.setTimeout(() => (active = true), 1400) }) window.addEventListener("pointermove", onMove, { passive: true }) document.documentElement.addEventListener("pointerleave", onLeave) return () => { cleanupIntro() window.clearTimeout(timeout) cancelAnimationFrame(frame) window.removeEventListener("pointermove", onMove) document.documentElement.removeEventListener("pointerleave", onLeave) } }, []) let charIndex = 0 return (

{lines.join(" ")} {lines.map((line) => ( ))}

) }