refactor: replace GSAP animations with CSS animations across components

- Updated RootLayout to include a noise overlay class.
- Refactored LiveModerationFeed to use CSS animations for item entry.
- Simplified AmbientCanvas by removing WebGL and using pure CSS for ambient effects.
- Converted Chatbot to use CSS for floating window animations.
- Updated Card component to include a gradient border.
- Refactored PageTransition to use CSS for fade-scale animations.
- Enhanced SectionHeader with reveal-up animation class.
- Replaced GSAP animations in NavRail with CSS animations for item entry.
- Refactored VoiceStage to use CSS for speaker node animations and pulse rings.
- Removed GSAP dependencies from use-gsap-animation hook, implementing CSS-based stagger reveal.
This commit is contained in:
asepharyana
2026-08-26 19:43:57 +07:00
parent 192685e1ce
commit 611ba39973
18 changed files with 698 additions and 549 deletions
@@ -1,21 +1,15 @@
"use client";
import { useGSAP } from "@gsap/react";
import gsap from "gsap";
import { useRef } from "react";
if (typeof window !== "undefined") {
gsap.registerPlugin(useGSAP);
// Set default Linear-style crisp easing & duration
gsap.defaults({
ease: "power2.out",
duration: 0.35,
});
}
import { useEffect, useRef } from "react";
/**
* Reusable hook for staggered reveal animations on child elements.
* Linear-style: crisp entry, very subtle vertical displacement, no layout shift.
* CSS-based stagger reveal. Applies `.stagger-in` class with incremental
* `animation-delay` to matching children when they enter the viewport.
* No GSAP — pure CSS keyframes + IntersectionObserver.
*
* Add this to globals.css if not already present:
* @keyframes stagger-in { from { opacity:0; transform:translateY(6px) } to { opacity:1; transform:translateY(0) } }
* .stagger-in { opacity:0; animation: stagger-in 0.32s ease-out forwards }
*/
export function useStaggerReveal<T extends HTMLElement = HTMLDivElement>(
selector: string,
@@ -29,43 +23,51 @@ export function useStaggerReveal<T extends HTMLElement = HTMLDivElement>(
) {
const containerRef = useRef<T>(null);
useGSAP(
() => {
if (!containerRef.current) return;
const elements = containerRef.current.querySelectorAll(selector);
if (!elements || elements.length === 0) return;
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const prefersReduced = window.matchMedia(
"(prefers-reduced-motion: reduce)",
).matches;
if (prefersReduced) {
gsap.set(elements, { opacity: 1, y: 0 });
return;
}
const prefersReduced = window.matchMedia(
"(prefers-reduced-motion: reduce)",
).matches;
gsap.fromTo(
elements,
{
opacity: 0,
y: options?.y ?? 8,
},
{
opacity: 1,
y: 0,
duration: options?.duration ?? 0.32,
delay: options?.delay ?? 0.02,
stagger: options?.stagger ?? 0.03,
ease: "power2.out",
clearProps: "transform",
},
);
},
{
scope: containerRef,
dependencies: options?.dependencies ?? [],
revertOnUpdate: true,
},
);
const elements = container.querySelectorAll<HTMLElement>(selector);
if (!elements || elements.length === 0) return;
if (prefersReduced) {
elements.forEach((el) => {
el.style.opacity = "1";
el.style.transform = "none";
});
return;
}
const staggerSec = options?.stagger ?? 0.03;
const durationSec = options?.duration ?? 0.32;
const baseDelay = options?.delay ?? 0;
elements.forEach((el, i) => {
el.style.opacity = "0";
el.style.animationFillMode = "forwards";
el.style.animationTimingFunction = "ease-out";
el.style.animationName = "stagger-in";
el.style.animationDuration = `${durationSec}s`;
el.style.animationDelay = `${baseDelay + i * staggerSec}s`;
});
// Cleanup: remove inline styles so next re-render can re-apply
return () => {
elements.forEach((el) => {
el.style.removeProperty("opacity");
el.style.removeProperty("animation-name");
el.style.removeProperty("animation-duration");
el.style.removeProperty("animation-delay");
el.style.removeProperty("animation-fill-mode");
el.style.removeProperty("animation-timing-function");
});
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, options?.dependencies ?? []);
return containerRef;
}