"use client" import { useEffect } from "react" export default function GsapAnimations() { useEffect(() => { let ctx: ReturnType | null = null import("gsap").then(async ({ gsap }) => { const { ScrollTrigger } = await import("gsap/ScrollTrigger") const { Observer } = await import("gsap/Observer") gsap.registerPlugin(ScrollTrigger, Observer) ctx = gsap.context(() => { initMarquee(gsap, Observer) initCounters(gsap) initScrollReveal(gsap, ScrollTrigger) initButtonHovers(gsap) initSmoothScroll() }) }) return () => { ctx?.revert() } }, []) return null } function initMarquee( gsap: typeof import("gsap").gsap, Observer: typeof import("gsap/Observer").Observer, ) { const marqueeItems = gsap.utils.toArray(".marquee-scroll-item") if (!marqueeItems.length) return const groups = new Map() marqueeItems.forEach((item) => { const parent = item.parentElement if (!parent) return if (!groups.has(parent)) groups.set(parent, []) groups.get(parent)!.push(item) }) const instances: { marqueeTimeline: gsap.core.Timeline marqueeObject: { value: number } hoverPauseProxy: { value: number } isHoverPaused: boolean lastDirection: number }[] = [] groups.forEach((items, containerEl) => { if (!items.length) return const inst = { marqueeObject: { value: 1 }, hoverPauseProxy: { value: 1 }, isHoverPaused: false, lastDirection: 1, marqueeTimeline: gsap.timeline({ repeat: -1, onReverseComplete() { this.progress(1) }, }), } inst.marqueeTimeline.fromTo(items, { xPercent: 0 }, { xPercent: -100, duration: 50, ease: "none" }) const triggerSet = new Set() if (containerEl.classList.contains("marquee-hover-stop")) triggerSet.add(containerEl) containerEl.querySelectorAll(".marquee-hover-stop").forEach((el) => triggerSet.add(el)) if (triggerSet.size && window.matchMedia("(pointer: fine)").matches) { const setPaused = (state: boolean) => { inst.isHoverPaused = state const ts = inst.marqueeTimeline.timeScale() gsap.killTweensOf(inst.hoverPauseProxy) gsap.fromTo(inst.hoverPauseProxy, { value: state ? ts : 0 }, { value: state ? 0 : inst.lastDirection || 1, duration: 0.4, ease: "power2.out", onUpdate: () => inst.marqueeTimeline.timeScale(inst.hoverPauseProxy.value), }) } triggerSet.forEach((el) => { el.addEventListener("mouseenter", () => setPaused(true)) el.addEventListener("mouseleave", () => setPaused(false)) }) } instances.push(inst) }) if (instances.length) { Observer.create({ target: window, type: "wheel,scroll,touch", onChangeY: (self: { velocityY: number }) => { const velocity = gsap.utils.clamp(-40, 40, self.velocityY * 0.002) const dir = velocity < 0 ? -1 : 1 instances.forEach((inst) => { if (inst.isHoverPaused) return inst.marqueeTimeline.timeScale(velocity) inst.lastDirection = dir gsap.fromTo(inst.marqueeObject, { value: velocity }, { value: dir, duration: 1, ease: "power2.out", onUpdate: () => { if (!inst.isHoverPaused) inst.marqueeTimeline.timeScale(inst.marqueeObject.value) }, }) }) }, }) } if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { instances.forEach((inst) => inst.marqueeTimeline.pause()) } } function initCounters(gsap: typeof import("gsap").gsap) { const elements = gsap.utils.toArray(".count-up-number-animation") if (!elements.length) return elements.forEach((el, i) => { const target = parseFloat(el.getAttribute("data-count") || "100") const decimals = target % 1 !== 0 ? 1 : 0 gsap.fromTo(el, { textContent: "0" }, { textContent: String(target), duration: 2, ease: "power1.out", snap: decimals ? { textContent: 0.1 } : { textContent: 1 }, delay: i * 0.1, scrollTrigger: { trigger: el, start: "top 80%", once: true }, onUpdate() { const v = parseFloat(el.textContent || "0") el.textContent = v.toLocaleString(undefined, { minimumFractionDigits: decimals, maximumFractionDigits: decimals }) }, }) }) } function initScrollReveal( gsap: typeof import("gsap").gsap, ScrollTrigger: typeof import("gsap/ScrollTrigger").ScrollTrigger, ) { ScrollTrigger.batch(".animate-on-scroll", { onEnter: (batch) => gsap.to(batch, { opacity: 1, y: 0, stagger: 0.15, overwrite: true }), onLeave: (batch) => gsap.set(batch, { opacity: 0, y: 100, overwrite: true }), onEnterBack: (batch) => gsap.to(batch, { opacity: 1, y: 0, stagger: 0.15, overwrite: true }), onLeaveBack: (batch) => gsap.set(batch, { opacity: 0, y: -100, overwrite: true }), }) } function initButtonHovers(gsap: typeof import("gsap").gsap) { document.querySelectorAll(".primary-button, .secondary-button, .footer-button").forEach((btn) => { const icon = btn.querySelector(".button-icon-wrapper svg") if (!icon) return btn.addEventListener("mouseenter", () => { gsap.to(icon, { x: 3, duration: 0.3, ease: "power2.out" }) }) btn.addEventListener("mouseleave", () => { gsap.to(icon, { x: 0, duration: 0.3, ease: "power2.out" }) }) }) document.querySelectorAll(".footer-link, .footer-contact-link").forEach((link) => { const bg = link.querySelector(".footer-link-bg, .footer-contact-link-bg") if (!bg) return link.addEventListener("mouseenter", () => { gsap.to(bg, { width: "100%", duration: 0.3, ease: "power2.out" }) }) link.addEventListener("mouseleave", () => { gsap.to(bg, { width: "0%", duration: 0.3, ease: "power2.out" }) }) }) document.querySelectorAll(".contact-v1-link").forEach((link) => { const bg = link.querySelector(".contact-icon-bg") if (!bg) return link.addEventListener("mouseenter", () => { gsap.to(bg, { width: "100%", duration: 0.4, ease: "power2.out" }) }) link.addEventListener("mouseleave", () => { gsap.to(bg, { width: "0%", duration: 0.4, ease: "power2.out" }) }) }) } function initSmoothScroll() { document.documentElement.style.scrollBehavior = "smooth" }