@@ -15,9 +15,10 @@ import {
1515 NavigationMenuTrigger ,
1616} from "@/components/ui/NavigationMenu"
1717import { Container } from "@code0-tech/pictor"
18+ import { AnimatePresence , m as motion } from "motion/react"
1819import Image from "next/image"
1920import Link from "next/link"
20- import React , { useEffect , useRef , useState } from "react"
21+ import React , { useEffect , useLayoutEffect , useRef , useState } from "react"
2122import { NavigationCursor , type NavigationCursorHandle } from "./NavigationCursor"
2223import { NavigationLink } from "./NavigationLink"
2324import { NavigationSubMenu } from "./NavigationSubMenu"
@@ -30,9 +31,11 @@ type NavigationDesktopProps = {
3031
3132const NavigationDesktop : React . FC < NavigationDesktopProps > = ( { locale, items, buttons } ) => {
3233 const navTabsRef = useRef < HTMLDivElement > ( null )
34+ const submenuContentRef = useRef < HTMLDivElement > ( null )
3335 const cursorRef = useRef < NavigationCursorHandle > ( null )
3436 const suppressMenuOpenRef = useRef ( false )
3537 const [ activeMenuValue , setActiveMenuValue ] = useState < string | null > ( null )
38+ const [ submenuHeight , setSubmenuHeight ] = useState ( 0 )
3639 const { homeHref, navbarItems, navbarButtons } = useNavigationViewModel ( locale , items , buttons )
3740 const isScrolled = useNavigationScrollState ( {
3841 onScroll : ( ) => {
@@ -44,6 +47,13 @@ const NavigationDesktop: React.FC<NavigationDesktopProps> = ({ locale, items, bu
4447
4548 const navItemClassName = "relative z-50 flex cursor-pointer items-center gap-2 rounded-xl px-4 py-1 font-medium"
4649 const navTriggerClassName = cn ( navItemClassName , "h-auto pr-1 bg-transparent text-base text-white hover:bg-transparent focus:bg-transparent data-popup-open:bg-transparent data-open:bg-transparent" )
50+ const activeMenuItem = navbarItems . find ( ( item ) => item . title === activeMenuValue )
51+ const activeSubMenu = activeMenuItem ?. subMenu ?. length ? activeMenuItem . subMenu : null
52+ const shellTransition = {
53+ type : "spring" ,
54+ stiffness : 40 ,
55+ damping : 10 ,
56+ } as const
4757
4858 useEffect ( ( ) => {
4959 if ( ! isScrolled ) return
@@ -52,23 +62,36 @@ const NavigationDesktop: React.FC<NavigationDesktopProps> = ({ locale, items, bu
5262 cursorRef . current ?. hide ( )
5363 } , [ isScrolled ] )
5464
65+ useLayoutEffect ( ( ) => {
66+ const element = submenuContentRef . current
67+
68+ if ( ! element || ! activeSubMenu || ! isScrolled ) {
69+ setSubmenuHeight ( 0 )
70+ return
71+ }
72+
73+ const measure = ( ) => {
74+ setSubmenuHeight ( element . scrollHeight )
75+ }
76+
77+ measure ( )
78+
79+ const resizeObserver = new ResizeObserver ( measure )
80+ resizeObserver . observe ( element )
81+
82+ return ( ) => resizeObserver . disconnect ( )
83+ } , [ activeSubMenu , isScrolled ] )
84+
5585 const updateIndicatorFromElement = ( element : HTMLElement ) => {
5686 if ( ! navTabsRef . current ) return
5787 cursorRef . current ?. moveTo ( element , navTabsRef . current )
5888 }
5989
6090 return (
61- < div
62- className = { cn (
63- "fixed z-50 h-max w-full border-b transition-[padding,background-color,border-color,backdrop-filter] duration-200 ease-out" ,
64- isScrolled
65- ? "border-white/10 bg-primary/50 pt-1 backdrop-blur-lg"
66- : "border-transparent bg-transparent pt-3"
67- ) }
68- >
91+ < div className = "fixed z-50 h-max w-full pt-3" >
6992 < Container >
7093 < div
71- className = { cn ( "relative transition-[padding] duration-200 ease-out" , isScrolled ? "py-2" : "py-4" ) }
94+ className = "relative my-4"
7295 onPointerMove = { ( ) => {
7396 suppressMenuOpenRef . current = false
7497 } }
@@ -77,6 +100,40 @@ const NavigationDesktop: React.FC<NavigationDesktopProps> = ({ locale, items, bu
77100 setActiveMenuValue ( null )
78101 } }
79102 >
103+ < motion . div
104+ className = { cn (
105+ "pointer-events-none absolute inset-0 rounded-2xl shadow-sm" ,
106+ isScrolled ? "bg-primary/50 backdrop-blur-lg" : "bg-transparent shadow-none" ,
107+ ) }
108+ initial = { {
109+ clipPath : "inset(0% 0% 0% 0% round 1rem)" ,
110+ } }
111+ animate = { {
112+ clipPath : isScrolled
113+ ? "inset(0% 10% 0% 10% round 1rem)"
114+ : "inset(0% 0% 0% 0% round 1rem)" ,
115+ } }
116+ transition = { shellTransition }
117+ />
118+ < motion . div
119+ className = "pointer-events-none absolute inset-0 z-10 rounded-2xl border border-white/5"
120+ initial = { false }
121+ animate = { {
122+ left : isScrolled ? "10%" : "0%" ,
123+ right : isScrolled ? "10%" : "0%" ,
124+ opacity : isScrolled ? 1 : 0 ,
125+ } }
126+ transition = { shellTransition }
127+ />
128+ < motion . div
129+ className = "relative z-10 flex flex-col overflow-visible rounded-2xl p-1.5"
130+ initial = { false }
131+ animate = { {
132+ paddingLeft : isScrolled ? "10%" : "0%" ,
133+ paddingRight : isScrolled ? "calc(10% + 0.5rem)" : "0%" ,
134+ } }
135+ transition = { shellTransition }
136+ >
80137 < div className = { "w-full h-full flex items-center justify-between gap-2" } >
81138
82139 < Link href = { homeHref } >
@@ -100,6 +157,7 @@ const NavigationDesktop: React.FC<NavigationDesktopProps> = ({ locale, items, bu
100157 value = { activeMenuValue }
101158 onValueChange = { ( value ) => {
102159 if ( value && suppressMenuOpenRef . current ) return
160+ if ( isScrolled && ! value ) return
103161
104162 setActiveMenuValue ( value )
105163 } }
@@ -119,19 +177,33 @@ const NavigationDesktop: React.FC<NavigationDesktopProps> = ({ locale, items, bu
119177 < >
120178 < div
121179 className = "relative z-50"
122- onMouseEnter = { ( event ) => updateIndicatorFromElement ( event . currentTarget ) }
123- onFocusCapture = { ( event ) => updateIndicatorFromElement ( event . currentTarget ) }
180+ onMouseEnter = { ( event ) => {
181+ updateIndicatorFromElement ( event . currentTarget )
182+
183+ if ( isScrolled && ! suppressMenuOpenRef . current ) {
184+ setActiveMenuValue ( itemValue )
185+ }
186+ } }
187+ onFocusCapture = { ( event ) => {
188+ updateIndicatorFromElement ( event . currentTarget )
189+
190+ if ( isScrolled ) {
191+ setActiveMenuValue ( itemValue )
192+ }
193+ } }
124194 >
125195 < NavigationMenuTrigger className = { navTriggerClassName } >
126196 { item . title }
127197 </ NavigationMenuTrigger >
128198 </ div >
129- < NavigationMenuContent className = "p-2" >
130- < NavigationSubMenu
131- items = { item . subMenu ! }
132- onSelect = { ( ) => setActiveMenuValue ( null ) }
133- />
134- </ NavigationMenuContent >
199+ { ! isScrolled && (
200+ < NavigationMenuContent className = "p-2" >
201+ < NavigationSubMenu
202+ items = { item . subMenu ! }
203+ onSelect = { ( ) => setActiveMenuValue ( null ) }
204+ />
205+ </ NavigationMenuContent >
206+ ) }
135207 </ >
136208 ) : item . href ? (
137209 < div
@@ -169,6 +241,29 @@ const NavigationDesktop: React.FC<NavigationDesktopProps> = ({ locale, items, bu
169241 ) ) }
170242 </ div >
171243 </ div >
244+ < AnimatePresence initial = { false } >
245+ { isScrolled && activeSubMenu && (
246+ < motion . div
247+ initial = { { opacity : 0 , y : - 6 , height : 0 } }
248+ animate = { { opacity : 1 , y : 0 , height : submenuHeight } }
249+ exit = { { opacity : 0 , y : - 6 , height : 0 } }
250+ transition = { {
251+ height : { duration : 0.24 , ease : [ 0.22 , 1 , 0.36 , 1 ] } ,
252+ opacity : { duration : 0.16 , ease : "easeOut" } ,
253+ y : { duration : 0.2 , ease : [ 0.22 , 1 , 0.36 , 1 ] } ,
254+ } }
255+ className = "overflow-hidden px-2"
256+ >
257+ < div ref = { submenuContentRef } className = "pt-1" >
258+ < NavigationSubMenu
259+ items = { activeSubMenu }
260+ onSelect = { ( ) => setActiveMenuValue ( null ) }
261+ />
262+ </ div >
263+ </ motion . div >
264+ ) }
265+ </ AnimatePresence >
266+ </ motion . div >
172267 </ div >
173268 </ Container >
174269 </ div >
0 commit comments