Files
doormile-next/src/animations/AnimationProvider.tsx
2026-06-02 14:10:44 +05:30

130 lines
3.9 KiB
TypeScript

"use client";
import React, { useEffect } from "react";
import { usePathname } from "next/navigation";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
/**
* AnimationProvider
* Initializes GSAP + ScrollTrigger globally, refreshes on route changes,
* and provides smooth defaults.
*/
export default function AnimationProvider({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
const initDecorativeBlocks = () => {
// Clean up previous block triggers to avoid duplicates
ScrollTrigger.getAll().forEach((t) => {
if (t.vars && (t.vars as any).id === "block-deco") {
t.kill();
}
});
const vh = window.innerHeight || document.documentElement.clientHeight;
const decorativeBlocks = document.querySelectorAll(".block-decoration");
decorativeBlocks.forEach((block) => {
ScrollTrigger.create({
id: "block-deco",
trigger: block,
start: "top 92%",
onEnter: () => {
setTimeout(() => {
block.classList.add("animated");
}, 150);
},
onEnterBack: () => {
setTimeout(() => {
block.classList.add("animated");
}, 150);
},
onLeave: () => {
block.classList.remove("animated");
},
onLeaveBack: () => {
block.classList.remove("animated");
},
});
// ScrollTrigger does not fire onEnter for blocks already in view at
// creation — on larger / taller screens those stayed un-animated.
// Reveal any block already intersecting the viewport.
const r = block.getBoundingClientRect();
if (r.top < vh && r.bottom > 0) {
block.classList.add("animated");
}
});
};
useEffect(() => {
gsap.registerPlugin(ScrollTrigger);
// Global GSAP defaults for buttery animations
gsap.defaults({
ease: "power3.out",
duration: 0.8,
});
// Mobile browsers fire `resize` when the address bar shows/hides while
// scrolling. Refreshing ScrollTrigger on every one of those caused jumpy /
// re-hidden animations on phones. Ignore those spurious resizes.
ScrollTrigger.config({ ignoreMobileResize: true });
// Run initializations
initDecorativeBlocks();
// Refresh on full window load (when all images, styles, and fonts are in place)
const handleLoad = () => {
initDecorativeBlocks();
ScrollTrigger.refresh(true);
};
window.addEventListener("load", handleLoad);
// Refresh on window resize — debounced so dragging the window across
// breakpoints recomputes trigger positions once it settles, instead of
// thrashing on every intermediate pixel.
let resizeTimer: ReturnType<typeof setTimeout>;
const handleResize = () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
initDecorativeBlocks();
ScrollTrigger.refresh(true);
}, 200);
};
window.addEventListener("resize", handleResize);
return () => {
clearTimeout(resizeTimer);
window.removeEventListener("load", handleLoad);
window.removeEventListener("resize", handleResize);
ScrollTrigger.getAll().forEach((t) => t.kill());
};
}, []);
// Listen for route changes and refresh ScrollTriggers so newly rendered content animates in correctly
useEffect(() => {
const refreshAnimations = () => {
initDecorativeBlocks();
ScrollTrigger.refresh(true);
};
// Run route change handler immediately on navigation
refreshAnimations();
// Staggered refreshes to accommodate page layout calculations and paint frames
const timers = [
setTimeout(refreshAnimations, 100),
setTimeout(refreshAnimations, 400),
setTimeout(refreshAnimations, 800),
setTimeout(refreshAnimations, 1500),
];
return () => {
timers.forEach(clearTimeout);
};
}, [pathname]);
return <>{children}</>;
}