"use client"; import { useId, useRef, type ElementType, type PointerEvent, type ReactNode } from "react"; import { motion, useMotionValue, useReducedMotion, useSpring, useTransform } from "motion/react"; type HeroSplitProps = { title: string; /** The picture or video side. Fills a 4:3 panel. */ media: ReactNode; /** Small line above the title. */ eyebrow?: string; subtitle?: string; /** Buttons or links under the text. */ children?: ReactNode; /** Small card that floats over the corner of the media. */ badge?: ReactNode; /** Put the media on the left. */ reverse?: boolean; /** h1 by default. Use h2 if the page already has an h1. */ headingAs?: ElementType; /** Set the background and text colour here. */ className?: string; }; const EASE = [0.16, 1, 0.3, 1] as const; const SPRING = { stiffness: 120, damping: 18 }; /* HeroSplit: text on one side, media on the other. The media wipes open, then leans gently toward the pointer. Stacks when narrow. */ export function HeroSplit({ title, media, eyebrow, subtitle, children, badge, reverse = false, headingAs: Heading = "h1", className = "bg-[#0b0a14] text-white", }: HeroSplitProps) { const reduce = useReducedMotion(); const id = useId(); const panel = useRef(null); const x = useMotionValue(0); const y = useMotionValue(0); const rotateY = useSpring(useTransform(x, [-0.5, 0.5], [-5, 5]), SPRING); const rotateX = useSpring(useTransform(y, [-0.5, 0.5], [4, -4]), SPRING); const badgeX = useSpring(useTransform(x, [-0.5, 0.5], [-10, 10]), SPRING); const badgeY = useSpring(useTransform(y, [-0.5, 0.5], [-10, 10]), SPRING); const move = (e: PointerEvent) => { const box = panel.current?.getBoundingClientRect(); if (reduce || e.pointerType !== "mouse" || !box) return; x.set(Math.max(-0.5, Math.min(0.5, (e.clientX - box.left) / box.width - 0.5))); y.set(Math.max(-0.5, Math.min(0.5, (e.clientY - box.top) / box.height - 0.5))); }; const leave = () => { x.set(0); y.set(0); }; const rise = (delay: number) => reduce ? { initial: { opacity: 0 }, animate: { opacity: 1 }, transition: { duration: 0.4 } } : { initial: { opacity: 0, y: 22 }, animate: { opacity: 1, y: 0 }, transition: { duration: 0.7, delay, ease: EASE } }; const closed = reverse ? "inset(0 0 0 100% round 20px)" : "inset(0 100% 0 0 round 20px)"; return (
{eyebrow && ( {eyebrow} )} {title} {subtitle && ( {subtitle} )} {children && ( {children} )}
{media} {badge && ( {badge} )}
); }