diff --git a/src/components/Navigation.tsx b/src/components/Navigation.tsx index cbae970..99e3c02 100644 --- a/src/components/Navigation.tsx +++ b/src/components/Navigation.tsx @@ -1,8 +1,9 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef, useCallback } from "react"; import Link from "next/link"; import { usePathname } from "next/navigation"; +import gsap from "gsap"; import { Menu, X } from "lucide-react"; import { site, navLinks, socialLinks } from "../data/site"; import { Icon } from "@/lib/icons"; @@ -11,6 +12,18 @@ export default function Navigation() { const [scrolled, setScrolled] = useState(false); const [mobileOpen, setMobileOpen] = useState(false); const pathname = usePathname(); + const menuRef = useRef(null); + const backdropRef = useRef(null); + const linksRef = useRef(null); + const socialsRef = useRef(null); + const timelineRef = useRef(null); + const prefersReducedMotion = useRef(false); + + useEffect(() => { + prefersReducedMotion.current = window.matchMedia( + "(prefers-reduced-motion: reduce)" + ).matches; + }, []); useEffect(() => { const handleScroll = () => setScrolled(window.scrollY > 50); @@ -24,6 +37,130 @@ export default function Navigation() { window.scrollTo(0, 0); }, [pathname]); + useEffect(() => { + if (mobileOpen) { + document.body.style.overflow = "hidden"; + } else { + document.body.style.overflow = ""; + } + return () => { + document.body.style.overflow = ""; + }; + }, [mobileOpen]); + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "Escape" && mobileOpen) { + setMobileOpen(false); + } + }; + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, [mobileOpen]); + + const animateOpen = useCallback(() => { + const menu = menuRef.current; + const backdrop = backdropRef.current; + const links = linksRef.current; + const socials = socialsRef.current; + if (!menu || !backdrop || !links || !socials) return; + + if (prefersReducedMotion.current) { + gsap.set(menu, { opacity: 1, y: 0 }); + gsap.set(backdrop, { opacity: 1 }); + gsap.set([...links.children, ...socials.children], { opacity: 1, y: 0 }); + return; + } + + const tl = gsap.timeline(); + timelineRef.current = tl; + + tl.set(menu, { display: "block" }) + .fromTo( + backdrop, + { opacity: 0 }, + { opacity: 1, duration: 0.2, ease: "power2.out" } + ) + .fromTo( + menu, + { opacity: 0, y: -10 }, + { opacity: 1, y: 0, duration: 0.25, ease: "power2.out" }, + "<" + ) + .fromTo( + links.children, + { opacity: 0, y: 12 }, + { + opacity: 1, + y: 0, + duration: 0.3, + stagger: 0.04, + ease: "power2.out", + }, + "-=0.1" + ) + .fromTo( + socials.children, + { opacity: 0, y: 8 }, + { + opacity: 1, + y: 0, + duration: 0.25, + stagger: 0.04, + ease: "power2.out", + }, + "-=0.15" + ); + }, []); + + const animateClose = useCallback(() => { + const menu = menuRef.current; + const backdrop = backdropRef.current; + const links = linksRef.current; + const socials = socialsRef.current; + if (!menu || !backdrop || !links || !socials) return; + + if (timelineRef.current) { + timelineRef.current.kill(); + } + + if (prefersReducedMotion.current) { + gsap.set(menu, { display: "none", opacity: 0 }); + gsap.set(backdrop, { opacity: 0, display: "none" }); + return; + } + + const tl = gsap.timeline({ + onComplete: () => { + gsap.set(menu, { display: "none" }); + gsap.set(backdrop, { display: "none" }); + }, + }); + + tl.to(links.children, { + opacity: 0, + y: -6, + duration: 0.15, + stagger: 0.02, + ease: "power2.in", + }) + .to( + socials.children, + { opacity: 0, duration: 0.12, stagger: 0.02, ease: "power2.in" }, + "<" + ) + .to(menu, { opacity: 0, y: -8, duration: 0.2, ease: "power2.in" }, "-=0.08") + .to(backdrop, { opacity: 0, duration: 0.2, ease: "power2.in" }, "<"); + }, []); + + useEffect(() => { + if (mobileOpen) { + animateOpen(); + } else { + animateClose(); + } + }, [mobileOpen, animateOpen, animateClose]); + return (
setMobileOpen(!mobileOpen)} - aria-label="Toggle menu" + aria-label={mobileOpen ? "Close menu" : "Open menu"} + aria-expanded={mobileOpen} > - {mobileOpen ? : } + + + {/* Backdrop */} +
setMobileOpen(false)} + aria-hidden="true" + /> + {/* Mobile Menu */} - {mobileOpen && ( -
-
    - {navLinks.map((link) => ( -
  • - setMobileOpen(false)} - > - {link.label} - -
  • - ))} -
- + {link.label} + + + ))} + +
+ {socialLinks.map((link) => ( + + + + ))}
- )} +
); }