"use client"; import { useLayoutEffect, useRef, useState } from "react"; import Link from "next/link"; import { gsap } from "gsap"; import { GoArrowUpRight } from "react-icons/go"; import { useTheme } from "next-themes"; import "./CardNav.css"; interface CardNavLink { label: string; href?: string; ariaLabel: string; } interface CardNavItem { label: string; bgColor: string; textColor: string; links: CardNavLink[]; } interface CardNavProps { logo?: string; logoAlt?: string; items: CardNavItem[]; className?: string; ease?: string; baseColor?: string; menuColor?: string; buttonBgColor?: string; buttonTextColor?: string; } const CardNav: React.FC = ({ logo, logoAlt = "Logo", items, className = "", ease = "power3.out", baseColor = "#fff", menuColor, buttonBgColor, buttonTextColor, }) => { const [isHamburgerOpen, setIsHamburgerOpen] = useState(false); const [isExpanded, setIsExpanded] = useState(false); const [isMobile, setIsMobile] = useState(false); const navRef = useRef(null); const cardsRef = useRef([]); const tlRef = useRef(null); const { theme, setTheme } = useTheme(); const calculateHeight = () => { const navEl = navRef.current; if (!navEl) return 260; const isMobile = window.matchMedia("(max-width: 768px)").matches; if (isMobile) { const contentEl = navEl.querySelector(".card-nav-content"); if (contentEl) { const wasVisible = contentEl.style.visibility; const wasPointerEvents = contentEl.style.pointerEvents; const wasPosition = contentEl.style.position; const wasHeight = contentEl.style.height; contentEl.style.visibility = "visible"; contentEl.style.pointerEvents = "auto"; contentEl.style.position = "static"; contentEl.style.height = "auto"; contentEl.offsetHeight; const topBar = 60; const padding = 16; const contentHeight = contentEl.scrollHeight; contentEl.style.visibility = wasVisible; contentEl.style.pointerEvents = wasPointerEvents; contentEl.style.position = wasPosition; contentEl.style.height = wasHeight; return topBar + contentHeight + padding; } } return 260; }; const createTimeline = () => { const navEl = navRef.current; if (!navEl) return null; gsap.set(navEl, { height: 60 }); gsap.set(cardsRef.current, { y: 50, opacity: 0 }); const tl = gsap.timeline({ paused: true }); tl.to(navEl, { height: calculateHeight, duration: 0.4, ease, }); tl.to( cardsRef.current, { y: 0, opacity: 1, duration: 0.4, ease, stagger: 0.08 }, "-=0.1" ); return tl; }; useLayoutEffect(() => { const tl = createTimeline(); tlRef.current = tl; return () => { tl?.kill(); tlRef.current = null; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [ease, items]); useLayoutEffect(() => { const handleResize = () => { if (!tlRef.current) return; if (isExpanded) { const newHeight = calculateHeight(); gsap.set(navRef.current, { height: newHeight }); tlRef.current.kill(); const newTl = createTimeline(); if (newTl) { newTl.progress(1); tlRef.current = newTl; } } else { tlRef.current.kill(); const newTl = createTimeline(); if (newTl) { tlRef.current = newTl; } } }; window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); // eslint-disable-next-line react-hooks/exhaustive-deps }, [isExpanded]); useLayoutEffect(() => { const mql = window.matchMedia("(max-width: 768px)"); const update = () => setIsMobile(mql.matches); update(); mql.addEventListener("change", update); return () => mql.removeEventListener("change", update); }, []); const toggleMenu = () => { const tl = tlRef.current; if (!tl) return; if (!isExpanded) { setIsHamburgerOpen(true); setIsExpanded(true); tl.play(0); } else { setIsHamburgerOpen(false); tl.eventCallback("onReverseComplete", () => setIsExpanded(false)); tl.reverse(); } }; const setCardRef = (i: number) => (el: HTMLDivElement | null) => { if (el) cardsRef.current[i] = el; }; return (
); }; export default CardNav;