"use client"; import { type ReactNode, useRef } from "react"; import gsap from "gsap"; import { useGSAP } from "@gsap/react"; import { CustomEase } from "gsap/CustomEase"; import { ScrollToPlugin } from "gsap/ScrollToPlugin"; import { ScrollTrigger } from "gsap/ScrollTrigger"; import { SplitText } from "gsap/SplitText"; gsap.registerPlugin(useGSAP, ScrollTrigger, ScrollToPlugin, SplitText, CustomEase); const bossEase = CustomEase.create("boss-ease", "M0,0 C0.18,0.84 0.32,1 1,1"); type BossAiMotionProps = { children: ReactNode; }; export function BossAiMotion({ children }: BossAiMotionProps) { const scopeRef = useRef(null); useGSAP( () => { const root = scopeRef.current; if (!root) { return; } gsap.set("[data-motion]", { autoAlpha: 1 }); gsap.set("[data-motion-line]", { transformOrigin: "center" }); const matchMedia = gsap.matchMedia(); matchMedia.add("(prefers-reduced-motion: reduce)", () => { gsap.set("[data-motion], [data-motion-dot], [data-motion-orbit], [data-motion-line]", { autoAlpha: 1, clearProps: "transform", }); }); matchMedia.add("(prefers-reduced-motion: no-preference)", () => { const heroTitle = root.querySelector(".hero-copy h1"); const splitTitle = heroTitle ? SplitText.create(heroTitle, { type: "words", aria: "auto", wordsClass: "hero-word", }) : null; const intro = gsap.timeline({ defaults: { ease: bossEase } }); intro .from("[data-motion='nav']", { autoAlpha: 0, y: -18, duration: 0.7 }) .from(".section-kicker", { autoAlpha: 0, y: 18, duration: 0.56 }, "-=0.18") .from( splitTitle?.words ?? ".hero-copy h1", { autoAlpha: 0, yPercent: 92, rotateX: -18, transformOrigin: "50% 100%", duration: 0.82, stagger: { each: 0.035, from: "start" }, }, "-=0.28", ) .from(".hero-copy p", { autoAlpha: 0, y: 22, duration: 0.7 }, "-=0.5") .from(".hero-actions > *", { autoAlpha: 0, y: 14, duration: 0.46, stagger: 0.08 }, "-=0.42") .from(".hero-proof > div", { autoAlpha: 0, y: 16, duration: 0.5, stagger: 0.07 }, "-=0.28") .from("[data-motion='hero-console']", { autoAlpha: 0, y: 34, rotateX: 5, duration: 0.9 }, "-=0.78") .from("[data-motion='route-row']", { autoAlpha: 0, x: 24, duration: 0.55, stagger: 0.08 }, "-=0.48") .from("[data-motion='node']", { autoAlpha: 0, scale: 0.94, duration: 0.45, stagger: 0.08 }, "-=0.3"); gsap.to("[data-motion-orbit]", { rotate: 360, duration: 28, ease: "none", repeat: -1, }); gsap.to("[data-motion-dot]", { scale: 1.18, autoAlpha: 0.72, duration: 1.4, ease: "sine.inOut", repeat: -1, yoyo: true, stagger: 0.24, }); gsap.to("[data-motion-line]", { strokeDashoffset: -160, duration: 5.8, ease: "none", repeat: -1, stagger: 0.28, }); const heroSection = root.querySelector(".hero-section"); const heroConsole = root.querySelector(".hero-console"); const routeRows = gsap.utils.toArray(".route-row", root); const consoleTiles = gsap.utils.toArray(".console-metrics > div, .mini-grid > div", root); if (heroSection && heroConsole) { gsap.set(heroConsole, { "--scan-y": "12%", "--scan-opacity": 0.16, "--grid-opacity": 0.16, } as gsap.TweenVars); gsap.to(heroConsole, { yPercent: -4, scale: 0.985, ease: "none", scrollTrigger: { trigger: heroSection, start: "top top", end: "bottom top", scrub: 0.85, }, }); gsap.to(heroConsole, { "--scan-y": "88%", "--scan-opacity": 0.38, "--grid-opacity": 0.34, ease: "none", scrollTrigger: { trigger: heroSection, start: "top 18%", end: "bottom 18%", scrub: 0.7, }, } as gsap.TweenVars); gsap.to(routeRows, { x: (index) => (index % 2 === 0 ? -4 : 4), y: (index) => -2 - index * 0.9, autoAlpha: (index) => 1 - index * 0.015, ease: "none", stagger: 0.03, scrollTrigger: { trigger: heroConsole, start: "top 76%", end: "bottom 18%", scrub: true, }, }); gsap.to(consoleTiles, { y: -12, autoAlpha: 0.9, ease: "none", stagger: { each: 0.035, from: "end" }, scrollTrigger: { trigger: heroConsole, start: "top 72%", end: "bottom 24%", scrub: true, }, }); } gsap.utils.toArray("[data-motion='section']").forEach((section) => { gsap.from(section, { autoAlpha: 0, y: 42, duration: 0.82, ease: bossEase, scrollTrigger: { trigger: section, start: "top 82%", once: true, }, }); }); gsap.utils .toArray("[data-motion='card']") .filter((card) => !card.closest(".hero-section")) .forEach((card, index) => { gsap.from(card, { autoAlpha: 0, y: 28, scale: 0.97, duration: 0.62, delay: (index % 4) * 0.04, ease: bossEase, scrollTrigger: { trigger: card, start: "top 88%", once: true, }, }); }); const cleanupAnchors = setupSmoothAnchors(root); const cleanupActiveNav = setupActiveNavigation(root); const cleanupMagneticCards = window.matchMedia("(hover: hover) and (pointer: fine)").matches ? setupMagneticCards(root) : () => undefined; return () => { cleanupMagneticCards(); cleanupActiveNav(); cleanupAnchors(); splitTitle?.revert(); }; }); return () => matchMedia.revert(); }, { scope: scopeRef }, ); return (
{children}
); } function setupSmoothAnchors(root: HTMLElement) { const anchors = gsap.utils.toArray("a[href^='#']", root); const cleanups = anchors.map((anchor) => { const onClick = (event: MouseEvent) => { const href = anchor.getAttribute("href"); const targetId = href?.slice(1); const target = targetId ? document.getElementById(targetId) : null; if (!href || !target) { return; } event.preventDefault(); window.history.pushState(null, "", href); gsap.to(window, { duration: 0.92, ease: bossEase, overwrite: "auto", scrollTo: { y: target, offsetY: 22 }, }); }; anchor.addEventListener("click", onClick); return () => anchor.removeEventListener("click", onClick); }); return () => cleanups.forEach((cleanup) => cleanup()); } function setupActiveNavigation(root: HTMLElement) { const links = gsap.utils.toArray(".nav-links a[href^='#']", root); const linkById = new Map( links .map((link) => [link.getAttribute("href")?.slice(1), link] as const) .filter((entry): entry is readonly [string, HTMLAnchorElement] => Boolean(entry[0])), ); const setActive = (id: string) => { links.forEach((link) => link.classList.toggle("is-active", linkById.get(id) === link)); }; const triggers = gsap.utils .toArray("section[id]", root) .filter((section) => linkById.has(section.id)) .map((section) => ScrollTrigger.create({ trigger: section, start: "top 42%", end: "bottom 42%", onEnter: () => setActive(section.id), onEnterBack: () => setActive(section.id), }), ); return () => { triggers.forEach((trigger) => trigger.kill()); links.forEach((link) => link.classList.remove("is-active")); }; } function setupMagneticCards(root: HTMLElement) { const cards = gsap.utils.toArray("[data-motion='card']", root); const cleanups = cards.map((card) => { const onPointerMove = (event: PointerEvent) => { const rect = card.getBoundingClientRect(); const xRatio = (event.clientX - rect.left) / rect.width - 0.5; const yRatio = (event.clientY - rect.top) / rect.height - 0.5; gsap.to(card, { x: xRatio * 10, y: yRatio * 10, rotateX: yRatio * -3, rotateY: xRatio * 4, duration: 0.38, ease: "power3.out", overwrite: "auto", }); }; const onPointerLeave = () => { gsap.to(card, { x: 0, y: 0, rotateX: 0, rotateY: 0, duration: 0.58, ease: bossEase, overwrite: "auto", }); }; card.addEventListener("pointermove", onPointerMove); card.addEventListener("pointerleave", onPointerLeave); return () => { card.removeEventListener("pointermove", onPointerMove); card.removeEventListener("pointerleave", onPointerLeave); }; }); return () => cleanups.forEach((cleanup) => cleanup()); }