"use client"; import Link from "next/link"; import { ChevronDown } from "lucide-react"; import { usePathname } from "next/navigation"; import { useEffect, useLayoutEffect, useRef, useState } from "react"; // ---------------------- // Type Definitions // ---------------------- type NavLinkProps = { text: string; href?: string; variant?: "desktop" | "mobile"; dropdown?: boolean; items?: { text: string; href: string }[]; }; // ---------------------- // Main Component // ---------------------- const HeaderNavLink = ({ text, href = "#", variant = "desktop", dropdown = false, items = [], }: NavLinkProps) => { const [open, setOpen] = useState(false); const [alignRight, setAlignRight] = useState(false); const wrapperRef = useRef(null); const menuRef = useRef(null); const pathname = usePathname(); // ---------------------- // Determine Active Link // ---------------------- const childIsActive = dropdown && items.length > 0 ? items.some((it) => pathname === it.href || pathname.startsWith(it.href)) : false; const isActive = (!dropdown && pathname === href) || childIsActive; // ---------------------- // Close Dropdown on Outside Click // ---------------------- useEffect(() => { if (!open) return; const onDown = (e: MouseEvent) => { if ( wrapperRef.current && !wrapperRef.current.contains(e.target as Node) ) { setOpen(false); } }; document.addEventListener("mousedown", onDown); return () => { document.removeEventListener("mousedown", onDown); }; }, [open]); // ---------------------- // Keep Desktop Dropdown Inside the Viewport // ---------------------- useLayoutEffect(() => { if (!open || variant !== "desktop") return; const updateAlignment = () => { const wrapper = wrapperRef.current; const menu = menuRef.current; if (!wrapper || !menu) return; const padding = 8; const { right, left } = wrapper.getBoundingClientRect(); const menuWidth = menu.offsetWidth; // Calculate the available space on either side of the trigger. const spaceRight = window.innerWidth - right - padding; const spaceLeft = left - padding; /* * Dropdowns normally open left-aligned with their trigger. * * If the menu would run off the right side of the viewport, and there * is more room available on the left, switch to right alignment. * * This is particularly useful for the last few navigation items, * where a wide dropdown could otherwise overflow the page. */ setAlignRight(spaceRight < menuWidth && spaceLeft > spaceRight); }; // useLayoutEffect runs after the menu has been rendered but before the // browser paints, allowing us to measure it and correct its position // without showing a visible alignment jump. updateAlignment(); // Recalculate if the browser width changes while the menu is open. window.addEventListener("resize", updateAlignment); return () => { window.removeEventListener("resize", updateAlignment); }; }, [open, variant, items.length]); // ---------------------- // Shared Link Styles // ---------------------- const base = variant === "mobile" ? "block w-full px-3 py-2 rounded-md transition text-left" : "flex items-center px-5 py-3 h-full transition relative"; const active = variant === "mobile" ? "bg-byu-navy/7 text-byu-navy font-semibold" : "border-b-2 md:border-b-3 border-byu-navy font-semibold"; // ---------------------- // Dropdown Navigation Item // ---------------------- if (dropdown && items.length > 0) { return (
variant === "desktop" && setOpen(true)} onMouseLeave={() => variant === "desktop" && setOpen(false)} > {/* Dropdown trigger */} {/* Dropdown menu */}
    {items.map((item) => { const itemActive = pathname === item.href || pathname.startsWith(item.href); return (
  • setOpen(false)} > {item.text}
  • ); })}
); } // ---------------------- // Standard Navigation Link // ---------------------- return ( {text} ); }; // ---------------------- // Export Component // ---------------------- export default HeaderNavLink;