"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); ScrollTrigger.config({ ignoreMobileResize: true }); const bossEase = CustomEase.create("boss-ease", "M0,0 C0.18,0.84 0.32,1 1,1"); const ANCHOR_NAV_EVENT = "bossai:anchor-nav"; type BossAiMotionProps = { children: ReactNode; }; export function BossAiMotion({ children }: BossAiMotionProps) { const scopeRef = useRef(null); useGSAP( () => { const root = scopeRef.current; if (!root) { return; } const select = gsap.utils.selector(root); try { root.classList.add("motion-enhanced"); gsap.set(select("[data-motion]"), { autoAlpha: 1 }); gsap.set(select("[data-motion-line]"), { transformOrigin: "center" }); const matchMedia = gsap.matchMedia(); const cleanupAnchors = setupSmoothAnchors(root); const cleanupActiveNav = setupActiveNavigation(root); const cleanupCompactNav = setupCompactNavigation(root); matchMedia.add("(prefers-reduced-motion: reduce)", () => { root.classList.add("motion-ready"); gsap.set(select("[data-motion], [data-motion-dot], [data-motion-orbit], [data-motion-line]"), { autoAlpha: 1, clearProps: "transform", }); }); matchMedia.add("(prefers-reduced-motion: no-preference) and (max-width: 640px)", () => { root.classList.add("motion-ready"); gsap.set(select("[data-motion], [data-motion-dot], [data-motion-orbit], [data-motion-line]"), { autoAlpha: 1, clearProps: "transform", }); const mobileIntro = gsap.timeline({ defaults: { ease: bossEase, overwrite: "auto" } }); mobileIntro .from(select("[data-motion='nav'], [data-motion='mobile-nav']"), { y: -8, duration: 0.32 }) .from(select(".hero-copy > *"), { y: 10, duration: 0.34, stagger: 0.04 }, "-=0.18") .from(select("[data-motion='hero-console']"), { y: 14, duration: 0.38 }, "-=0.22"); gsap.to(select("[data-motion-dot]"), { scale: 1.14, autoAlpha: 0.76, duration: 1.4, ease: "sine.inOut", overwrite: "auto", repeat: -1, yoyo: true, stagger: 0.2, }); gsap.to(select("[data-motion-line]"), { strokeDashoffset: -120, duration: 6, ease: "none", overwrite: "auto", repeat: -1, stagger: 0.24, }); }); matchMedia.add("(prefers-reduced-motion: no-preference) and (min-width: 641px)", () => { root.classList.add("motion-ready"); 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, overwrite: "auto" } }); intro .from(select("[data-motion='nav']"), { y: -14, duration: 0.7 }) .from(select(".section-kicker"), { y: 14, duration: 0.56 }, "-=0.18") .from( splitTitle?.words ?? select(".hero-copy h1"), { yPercent: 18, rotationX: -6, transformOrigin: "50% 100%", duration: 0.68, stagger: { each: 0.035, from: "start" }, }, "-=0.28", ) .from(select(".hero-copy p"), { y: 18, duration: 0.7 }, "-=0.5") .from(select("[data-motion='signal']"), { y: 10, duration: 0.42, stagger: 0.06 }, "-=0.42") .from(select(".hero-actions > *"), { y: 12, duration: 0.46, stagger: 0.08 }, "-=0.42") .from(select(".hero-proof > div"), { y: 12, duration: 0.5, stagger: 0.07 }, "-=0.28") .from(select("[data-motion='hero-console']"), { y: 22, rotationX: 3, duration: 0.72 }, "-=0.68") .from(select("[data-motion='exchange-step']"), { y: 12, duration: 0.48, stagger: 0.06 }, "-=0.58") .from(select("[data-motion='route-row']"), { x: 14, duration: 0.55, stagger: 0.08 }, "-=0.48") .from(select("[data-motion='node']"), { scale: 0.96, duration: 0.45, stagger: 0.08 }, "-=0.3"); gsap.to(select("[data-motion-orbit]"), { rotation: 360, duration: 28, ease: "none", overwrite: "auto", repeat: -1, }); gsap.to(select("[data-motion-dot]"), { scale: 1.18, autoAlpha: 0.72, duration: 1.4, ease: "sine.inOut", overwrite: "auto", repeat: -1, yoyo: true, stagger: 0.24, }); gsap.to(select("[data-motion-line]"), { strokeDashoffset: -160, duration: 5.8, ease: "none", overwrite: "auto", 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 exchangeSteps = gsap.utils.toArray(".exchange-step", 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(exchangeSteps, { y: (index) => -5 + index * 1.4, autoAlpha: (index) => 0.98 - index * 0.02, ease: "none", stagger: { each: 0.035, from: "start" }, scrollTrigger: { trigger: heroConsole, start: "top 80%", end: "center 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']", root).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']", root) .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 cleanupMagneticCards = window.matchMedia("(hover: hover) and (pointer: fine)").matches ? setupMagneticCards(root) : () => undefined; return () => { cleanupMagneticCards(); splitTitle?.revert(); }; }); return () => { cleanupCompactNav(); cleanupActiveNav(); cleanupAnchors(); matchMedia.revert(); root.classList.remove("motion-enhanced", "motion-ready"); }; } catch (error) { console.warn("BossAI motion setup failed; using static website state.", error); root.classList.remove("motion-enhanced", "motion-ready"); } }, { scope: scopeRef }, ); return (
{children}
); } function setupSmoothAnchors(root: HTMLElement) { const anchors = gsap.utils.toArray("a[href^='#']", root); let activeScrollTween: gsap.core.Tween | null = null; let pendingFocusTarget: HTMLElement | null = null; let rootScrollStateTimer: number | null = null; const reduceMotionQuery = window.matchMedia("(prefers-reduced-motion: reduce)"); const readAnchorOffset = () => { const rawOffset = window.getComputedStyle(document.documentElement).getPropertyValue("--anchor-offset"); const parsedOffset = Number.parseFloat(rawOffset); return Number.isFinite(parsedOffset) ? parsedOffset : 22; }; const clearScrollState = () => { root.classList.remove("is-anchor-scrolling"); if (rootScrollStateTimer) { window.clearTimeout(rootScrollStateTimer); rootScrollStateTimer = null; } }; const focusTarget = (target: HTMLElement) => { const previousTabIndex = target.getAttribute("tabindex"); target.setAttribute("tabindex", "-1"); target.focus({ preventScroll: true }); if (previousTabIndex === null) { target.removeAttribute("tabindex"); } else { target.setAttribute("tabindex", previousTabIndex); } }; 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); window.dispatchEvent(new Event(ANCHOR_NAV_EVENT)); activeScrollTween?.kill(); pendingFocusTarget = target; if (reduceMotionQuery.matches) { target.scrollIntoView({ block: "start" }); focusTarget(target); pendingFocusTarget = null; clearScrollState(); return; } root.classList.add("is-anchor-scrolling"); if (rootScrollStateTimer) { window.clearTimeout(rootScrollStateTimer); } rootScrollStateTimer = window.setTimeout(clearScrollState, 1400); activeScrollTween = gsap.to(window, { duration: 0.92, ease: bossEase, overwrite: "auto", scrollTo: { y: target, offsetY: readAnchorOffset() }, onComplete: () => { if (pendingFocusTarget === target) { focusTarget(target); pendingFocusTarget = null; } clearScrollState(); }, onInterrupt: clearScrollState, }); }; anchor.addEventListener("click", onClick); return () => anchor.removeEventListener("click", onClick); }); return () => { activeScrollTween?.kill(); activeScrollTween = null; pendingFocusTarget = null; clearScrollState(); cleanups.forEach((cleanup) => cleanup()); }; } function setupActiveNavigation(root: HTMLElement) { const links = gsap.utils.toArray(".nav-links a[href^='#'], .mobile-nav-rail a[href^='#']", root); const linkedSectionIds = new Set( links .map((link) => link.getAttribute("href")?.slice(1)) .filter((id): id is string => Boolean(id)), ); const setActive = (id: string) => { links.forEach((link) => { const active = link.getAttribute("href") === `#${id}`; link.classList.toggle("is-active", active); if (active) { link.setAttribute("aria-current", "true"); } else { link.removeAttribute("aria-current"); } }); const activeMobileLink = links.find((link) => ( link.getAttribute("href") === `#${id}` && Boolean(link.closest(".mobile-nav-rail")) )); if (activeMobileLink) { centerMobileNavLink(activeMobileLink); } }; const syncActiveFromHash = () => { const activeId = window.location.hash.slice(1); if (activeId && linkedSectionIds.has(activeId)) { setActive(activeId); } }; const triggers = gsap.utils .toArray("section[id]", root) .filter((section) => linkedSectionIds.has(section.id)) .map((section) => ScrollTrigger.create({ trigger: section, start: "top 42%", end: "bottom 42%", onEnter: () => setActive(section.id), onEnterBack: () => setActive(section.id), }), ); syncActiveFromHash(); window.addEventListener("hashchange", syncActiveFromHash); window.addEventListener(ANCHOR_NAV_EVENT, syncActiveFromHash); return () => { window.removeEventListener("hashchange", syncActiveFromHash); window.removeEventListener(ANCHOR_NAV_EVENT, syncActiveFromHash); triggers.forEach((trigger) => trigger.kill()); links.forEach((link) => { link.classList.remove("is-active"); link.removeAttribute("aria-current"); }); }; } function centerMobileNavLink(link: HTMLAnchorElement) { const rail = link.closest(".mobile-nav-rail"); if (!rail) { return; } const nextScrollLeft = link.offsetLeft - (rail.clientWidth - link.clientWidth) / 2; rail.scrollTo({ left: Math.max(0, nextScrollLeft), behavior: window.matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth", }); } function setupCompactNavigation(root: HTMLElement) { const syncCompactState = () => { root.classList.toggle("is-nav-compact", window.scrollY > 18); }; syncCompactState(); window.addEventListener("scroll", syncCompactState, { passive: true }); return () => { window.removeEventListener("scroll", syncCompactState); root.classList.remove("is-nav-compact"); }; } 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(); if (!rect.width || !rect.height) { return; } 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 * 6, y: yRatio * 6, rotationX: yRatio * -2, rotationY: xRatio * 2.5, duration: 0.38, ease: "power3.out", overwrite: "auto", }); }; const onPointerLeave = () => { gsap.to(card, { x: 0, y: 0, rotationX: 0, rotationY: 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); gsap.killTweensOf(card); }; }); return () => cleanups.forEach((cleanup) => cleanup()); }