function initMegaNavDirectionalHover() { const DUR = { bgMorph: 0.4, contentIn: 0.3, contentOut: 0.2, stagger: 0.25, backdropIn: 0.3, backdropOut: 0.2, openScale: 0.35, closeScale: 0.25, }; const HOVER_ENTER = 120; const HOVER_LEAVE = 150; // DOM references const menuWrap = document.querySelector("[data-menu-wrap]"); const navList = document.querySelector("[data-nav-list]"); const dropWrapper = document.querySelector("[data-dropdown-wrapper]"); const dropContainer = document.querySelector("[data-dropdown-container]"); const dropBg = document.querySelector("[data-dropdown-bg]"); const backdrop = document.querySelector("[data-menu-backdrop]"); const toggles = [...document.querySelectorAll("[data-dropdown-toggle]")]; const panels = [...document.querySelectorAll("[data-nav-content]")]; const burger = document.querySelector("[data-burger-toggle]"); const backBtn = document.querySelector("[data-mobile-back]"); const logo = document.querySelector("[data-menu-logo]"); const [lineTop, lineMid, lineBot] = ["top", "mid", "bot"].map( (id) => document.querySelector(`[data-burger-line='${id}']`) ); // State const state = { isOpen: false, activePanel: null, activePanelIndex: -1, isMobile: window.innerWidth <= 991, mobileMenuOpen: false, mobilePanelActive: null, hoverTimer: null, leaveTimer: null, tl: null, mobileTl: null, mobilePanelTl: null, }; // Helpers const getPanel = (name) => document.querySelector(`[data-nav-content="${name}"]`); const getToggle = (name) => document.querySelector(`[data-dropdown-toggle="${name}"]`); const getFade = (el) => el.querySelectorAll("[data-menu-fade]"); const getNavItems = () => navList.querySelectorAll("[data-nav-list-item]"); const getIndex = (name) => toggles.indexOf(getToggle(name)); const stagger = (n) => (n <= 1 ? 0 : { amount: DUR.stagger }); function clearTimers() { clearTimeout(state.hoverTimer); clearTimeout(state.leaveTimer); state.hoverTimer = state.leaveTimer = null; } function killTl(key) { if (state[key]) { state[key].kill(); state[key] = null; } } function killDropdown() { killTl("tl"); gsap.killTweensOf(dropContainer); gsap.killTweensOf(backdrop); panels.forEach((p) => { gsap.killTweensOf(p); gsap.killTweensOf(getFade(p)); }); } function killMobile() { killTl("mobileTl"); gsap.killTweensOf([navList, lineTop, lineMid, lineBot]); } function killMobilePanel() { killTl("mobilePanelTl"); gsap.killTweensOf(getNavItems()); gsap.killTweensOf([backBtn, logo]); panels.forEach((p) => { gsap.killTweensOf(p); gsap.killTweensOf(getFade(p)); }); } function resetToggles() { toggles.forEach((t) => t.setAttribute("aria-expanded", "false")); } function resetDesktop() { panels.forEach((p) => { gsap.set(p, { visibility:"hidden", opacity:0, pointerEvents:"none", x:0, y:0, xPercent:0 }); gsap.set(getFade(p), { autoAlpha:0, x:0, y:0, xPercent:0 }); }); gsap.set(dropContainer, { height:0, clearProps:"transform" }); gsap.set(backdrop, { autoAlpha:0 }); menuWrap.setAttribute("data-menu-open", "false"); resetToggles(); } function setupMobile() { panels.forEach((p) => { gsap.set(p, { autoAlpha: 0, xPercent: 0, visibility: "visible", pointerEvents: "none" }); gsap.set(getFade(p), { xPercent: 20, autoAlpha: 0 }); }); gsap.set(getNavItems(), { xPercent: 0, y: 0, autoAlpha: 1 }); gsap.set(navList, { autoAlpha: 0, x: 0 }); gsap.set(backBtn, { autoAlpha: 0 }); gsap.set(logo, { autoAlpha: 1 }); gsap.set(dropContainer, { clearProps: "height" }); gsap.set(backdrop, { autoAlpha: 0 }); } function measurePanel(name) { const el = getPanel(name); if (!el) return 0; const s = el.style; const prev = [s.visibility, s.opacity, s.pointerEvents]; Object.assign(s, { visibility: "visible", opacity: "0", pointerEvents: "none" }); const h = el.getBoundingClientRect().height; [s.visibility, s.opacity, s.pointerEvents] = prev; return h; } // DESKTOP — open dropdown (first open) function openDropdown(panelName) { if (state.isOpen && state.activePanel === panelName) return; if (state.isOpen) return switchPanel(state.activePanel, panelName); const height = measurePanel(panelName); if (!height) return; killDropdown(); resetDesktop(); const el = getPanel(panelName); const fade = getFade(el); const toggle = getToggle(panelName); state.isOpen = true; state.activePanel = panelName; state.activePanelIndex = getIndex(panelName); menuWrap.setAttribute("data-menu-open", "true"); if (toggle) toggle.setAttribute("aria-expanded", "true"); gsap.set(dropContainer, { height: 0 }); const tl = gsap.timeline(); state.tl = tl; tl.to(backdrop, { autoAlpha: 1, duration: DUR.backdropIn, ease: "power2.out" }, 0); tl.to(dropContainer, { height, duration: DUR.openScale, ease: "power3.out" }, 0); tl.set(el, { visibility: "visible", opacity: 1, pointerEvents: "auto" }, 0.05); if (fade.length) { tl.fromTo(fade, { autoAlpha: 0, y: 8 }, { autoAlpha: 1, y: 0, duration: DUR.contentIn, stagger: stagger(fade.length), ease: "power3.out" }, 0.1 ); } } // DESKTOP — close dropdown function closeDropdown() { if (!state.isOpen) return; const el = getPanel(state.activePanel); const fade = el ? getFade(el) : []; killDropdown(); const tl = gsap.timeline({ onComplete() { state.isOpen = false; state.activePanel = null; state.activePanelIndex = -1; state.tl = null; resetDesktop(); }, }); state.tl = tl; if (fade.length) tl.to(fade, { autoAlpha: 0, y: -4, duration: DUR.contentOut * 0.7, ease: "power2.in" }, 0); tl.to(dropContainer, { height: 0, duration: DUR.closeScale, ease: "power2.in" }, 0.05); tl.to(backdrop, { autoAlpha: 0, duration: DUR.backdropOut, ease: "power2.out" }, 0); if (el) tl.set(el, { visibility: "hidden", opacity: 0, pointerEvents: "none" }); } // DESKTOP — switch panel (directional) function switchPanel(fromName, toName) { const dir = getIndex(toName) > getIndex(fromName) ? 1 : -1; const fromEl = getPanel(fromName), toEl = getPanel(toName); if (!fromEl || !toEl) return; const fromFade = getFade(fromEl), toFade = getFade(toEl); const toHeight = measurePanel(toName); if (!toHeight) return; killDropdown(); // Reset all panels, then restore fromEl as visible panels.forEach((p) => { gsap.set(p, { visibility: "hidden", opacity: 0, pointerEvents: "none", xPercent: 0 }); gsap.set(getFade(p), { autoAlpha: 0, x: 0, y: 0 }); }); gsap.set(fromEl, { visibility: "visible", opacity: 1, pointerEvents: "auto", x: 0 }); if (fromFade.length) gsap.set(fromFade, { autoAlpha: 1, x: 0, y: 0 }); gsap.set(backdrop, { autoAlpha: 1 }); const toToggle = getToggle(toName); state.activePanel = toName; state.activePanelIndex = getIndex(toName); resetToggles(); if (toToggle) toToggle.setAttribute("aria-expanded", "true"); const xOut = dir * -30, xIn = dir * 30; const tl = gsap.timeline(); state.tl = tl; if (fromFade.length) tl.to(fromFade, { autoAlpha: 0, x: xOut, duration: DUR.contentOut, ease: "power2.in" }, 0); tl.set(fromEl, { visibility: "hidden", opacity: 0, pointerEvents: "none", xPercent: 0 }, DUR.contentOut); if (fromFade.length) tl.set(fromFade, { x: 0 }, DUR.contentOut); tl.to(dropContainer, { height: toHeight, duration: DUR.bgMorph, ease: "power3.out" }, 0.05); tl.set(toEl, { visibility: "visible", opacity: 1, pointerEvents: "auto", xPercent: 0 }, DUR.contentOut * 0.5); if (toFade.length) { tl.fromTo(toFade, { autoAlpha: 0, x: xIn }, { autoAlpha: 1, x: 0, duration: DUR.contentIn, stagger: stagger(toFade.length), ease: "power3.out" }, DUR.contentOut * 0.6 ); } } // DESKTOP — hover intent function handleToggleEnter(e) { if (state.isMobile) return; const name = e.currentTarget.getAttribute("data-dropdown-toggle"); if (!name) return; clearTimeout(state.leaveTimer); state.leaveTimer = null; clearTimeout(state.hoverTimer); state.hoverTimer = setTimeout(() => openDropdown(name), state.isOpen ? 0 : HOVER_ENTER); } function handleToggleLeave() { if (state.isMobile) return; clearTimeout(state.hoverTimer); state.hoverTimer = null; state.leaveTimer = setTimeout(closeDropdown, HOVER_LEAVE); } function handleWrapperEnter() { if (state.isMobile) return; clearTimeout(state.leaveTimer); state.leaveTimer = null; } function handleWrapperLeave() { if (state.isMobile) return; state.leaveTimer = setTimeout(closeDropdown, HOVER_LEAVE); } // DESKTOP — close behaviors function handleEscape(e) { if (e.key !== "Escape") return; if (state.isMobile) { state.mobilePanelActive ? closeMobilePanel() : state.mobileMenuOpen && closeMobileMenu(); return; } if (state.isOpen) { const t = getToggle(state.activePanel); closeDropdown(); if (t) t.focus(); } } function handleDocClick(e) { if (state.isMobile || !state.isOpen) return; if (!e.target.closest("[data-menu-wrap]")) closeDropdown(); } // DESKTOP — keyboard navigation function focusFirstLink(panelName) { setTimeout(() => { const el = getPanel(panelName); if (!el) return; const link = el.querySelector("a"); if (!link) return; gsap.set(link, { visibility: "visible" }); link.focus(); }, 80); } function handleKeydownOnToggle(e) { if (state.isMobile) return; const name = e.currentTarget.getAttribute("data-dropdown-toggle"); if (e.key === "Enter" || e.key === " ") { e.preventDefault(); if (state.isOpen && state.activePanel === name) closeDropdown(); else { openDropdown(name); focusFirstLink(name); } return; } if (e.key === "ArrowDown") { e.preventDefault(); if (!state.isOpen || state.activePanel !== name) openDropdown(name); focusFirstLink(name); } if (e.key === "Tab" && !e.shiftKey && state.isOpen && state.activePanel === name) { e.preventDefault(); const link = getPanel(name)?.querySelector("a"); if (link) link.focus(); } } function handleKeydownInPanel(e) { if (state.isMobile || !state.isOpen) return; const el = getPanel(state.activePanel); if (!el) return; const links = [...el.querySelectorAll("a")]; const idx = links.indexOf(document.activeElement); if (e.key === "ArrowDown") { e.preventDefault(); links[(idx + 1) % links.length].focus(); } if (e.key === "ArrowUp") { e.preventDefault(); if (idx <= 0) { const t = getToggle(state.activePanel); if (t) t.focus(); } else links[idx - 1].focus(); } if (e.key === "Tab" && !e.shiftKey && idx === links.length - 1) { e.preventDefault(); const curIdx = toggles.indexOf(getToggle(state.activePanel)); const next = curIdx < toggles.length - 1 ? toggles[curIdx + 1] : null; closeDropdown(); if (next) next.focus(); } if (e.key === "Tab" && e.shiftKey && idx === 0) { e.preventDefault(); const t = getToggle(state.activePanel); if (t) t.focus(); } } // MOBILE — burger animation function animateBurger(toX) { const tl = gsap.timeline({ defaults: { ease: "power2.inOut" } }); if (toX) { tl.to(lineTop, { y: "0.3125em", duration: 0.15 }, 0); tl.to(lineBot, { y: "-0.3125em", duration: 0.15 }, 0); tl.to(lineMid, { autoAlpha: 0, duration: 0.1 }, 0.1); tl.to(lineTop, { rotation: 45, duration: 0.2 }, 0.15); tl.to(lineBot, { rotation: -45, duration: 0.2 }, 0.15); } else { tl.to(lineTop, { rotation: 0, duration: 0.2 }, 0); tl.to(lineBot, { rotation: 0, duration: 0.2 }, 0); tl.to(lineTop, { y: 0, duration: 0.15 }, 0.15); tl.to(lineBot, { y: 0, duration: 0.15 }, 0.15); tl.to(lineMid, { autoAlpha: 1, duration: 0.1 }, 0.15); } return tl; } // MOBILE — open/close menu function openMobileMenu() { killMobile(); state.mobileMenuOpen = true; menuWrap.setAttribute("data-menu-open", "true"); burger.setAttribute("aria-expanded", "true"); document.body.style.overflow = "hidden"; const items = getNavItems(); const tl = gsap.timeline(); state.mobileTl = tl; tl.add(animateBurger(true), 0); tl.to(navList, { autoAlpha: 1, duration: 0.3, ease: "power2.out" }, 0); if (items.length) { tl.fromTo(items, { autoAlpha: 0, y: 12 }, { autoAlpha: 1, y: 0, duration: 0.3, stagger: 0.04, ease: "power3.out" }, 0.15 ); } } function closeMobileMenu() { const hadPanel = state.mobilePanelActive; const panelEl = hadPanel ? getPanel(hadPanel) : null; killMobile(); killMobilePanel(); menuWrap.setAttribute("data-menu-open", "false"); state.mobileMenuOpen = false; state.mobilePanelActive = null; burger.setAttribute("aria-expanded", "false"); const tl = gsap.timeline({ onComplete() { document.body.style.overflow = ""; state.mobileTl = null; setupMobile(); }, }); state.mobileTl = tl; tl.add(animateBurger(false), 0); // If a panel was open, fade it out with the close — no snap reset if (hadPanel && panelEl) { tl.to(panelEl, { autoAlpha: 0, duration: 0.3, ease: "power2.inOut" }, 0.05); tl.to(backBtn, { autoAlpha: 0, duration: 0.2, ease: "power2.in" }, 0.05); } // Fade out the nav list container tl.to(navList, { autoAlpha: 0, duration: 0.3, ease: "power2.inOut" }, 0.05); } // MOBILE — slide-over panels function openMobilePanel(panelName) { const el = getPanel(panelName); if (!el) return; killMobilePanel(); state.mobilePanelActive = panelName; const navItems = getNavItems(); const panelFade = getFade(el); const tl = gsap.timeline(); state.mobilePanelTl = tl; // Fade out each nav item to the left if (navItems.length) { tl.to(navItems, { xPercent: -10, autoAlpha: 0, duration: 0.35, stagger: 0.03, ease: "power2.in", }, 0); } // Logo → back button swap tl.to(logo, { autoAlpha: 0, duration: 0.2, ease: "power2.in" }, 0); tl.to(backBtn, { autoAlpha: 1, duration: 0.25, ease: "power2.inOut" }, 0.15); // Show panel container, then fade in its items from the right tl.set(el, { autoAlpha: 1, xPercent: 0, pointerEvents: "auto" }, 0.2); if (panelFade.length) { tl.fromTo(panelFade, { xPercent: 8, autoAlpha: 0 }, { xPercent: 0, autoAlpha: 1, duration: 0.3, stagger: stagger(panelFade.length), ease: "power3.out" }, 0.25 ); } } function closeMobilePanel() { if (!state.mobilePanelActive) return; const el = getPanel(state.mobilePanelActive); if (!el) return; killMobilePanel(); const navItems = getNavItems(); const panelFade = getFade(el); const tl = gsap.timeline({ onComplete() { state.mobilePanelActive = null; state.mobilePanelTl = null; }, }); state.mobilePanelTl = tl; // Fade out panel items to the right if (panelFade.length) { tl.to(el, { xPercent: 20, autoAlpha: 0, duration: 0.3, stagger: 0.02, ease: "power2.in", }, 0); } // Hide panel tl.set(el, { autoAlpha: 0, pointerEvents: "none" }, 0.25); // Back → logo swap tl.to(backBtn, { autoAlpha: 0, duration: 0.2, ease: "power2.in" }, 0); tl.to(logo, { autoAlpha: 1, duration: 0.25, ease: "power2.out" }, 0.15); // Fade nav items back in from center if (navItems.length) { tl.fromTo(navItems, { xPercent: -20, autoAlpha: 0 }, { xPercent: 0, autoAlpha: 1, duration: 0.35, stagger: 0.03, ease: "power3.out" }, 0.25 ); } } function handleToggleClick(e) { if (!state.isMobile || !state.mobileMenuOpen) return; const name = e.currentTarget.getAttribute("data-dropdown-toggle"); if (name) { e.preventDefault(); openMobilePanel(name); } } // RESIZE let resizeTimer = null; let lastWidth = window.innerWidth; function handleResize() { const w = window.innerWidth; if (w === lastWidth) return; lastWidth = w; clearTimeout(resizeTimer); resizeTimer = setTimeout(() => { const was = state.isMobile; state.isMobile = window.innerWidth <= 991; if (was && !state.isMobile) { killMobile(); killMobilePanel(); gsap.set(navList, { clearProps: "all" }); gsap.set(getNavItems(), { clearProps: "all" }); gsap.set(backBtn, { autoAlpha: 0 }); gsap.set(logo, { clearProps: "all" }); gsap.set([lineTop, lineMid, lineBot], { rotation: 0, y: 0, autoAlpha: 1 }); panels.forEach((p) => { gsap.set(p, { clearProps: "all" }); gsap.set(getFade(p), { clearProps: "all" }); }); burger.setAttribute("aria-expanded", "false"); state.mobileMenuOpen = false; state.mobilePanelActive = null; document.body.style.overflow = ""; resetDesktop(); } if (!was && state.isMobile) { killDropdown(); state.isOpen = false; state.activePanel = null; state.activePanelIndex = -1; clearTimers(); menuWrap.setAttribute("data-menu-open", "false"); resetToggles(); setupMobile(); } }, 150); } // EVENT BINDING toggles.forEach((btn) => { btn.addEventListener("mouseenter", handleToggleEnter); btn.addEventListener("mouseleave", handleToggleLeave); btn.addEventListener("keydown", handleKeydownOnToggle); btn.addEventListener("click", handleToggleClick); }); dropWrapper.addEventListener("mouseenter", handleWrapperEnter); dropWrapper.addEventListener("mouseleave", handleWrapperLeave); panels.forEach((p) => p.addEventListener("keydown", handleKeydownInPanel)); backdrop.addEventListener("click", closeDropdown); document.addEventListener("keydown", handleEscape); document.addEventListener("click", handleDocClick); burger.addEventListener("click", () => state.mobileMenuOpen ? closeMobileMenu() : openMobileMenu()); backBtn.addEventListener("click", closeMobilePanel); window.addEventListener("resize", handleResize); // INIT state.isMobile ? setupMobile() : resetDesktop(); } // Initialize Mega Navigation (Directional Hover) document.addEventListener('DOMContentLoaded', function() { initMegaNavDirectionalHover(); });