@@ -4,6 +4,8 @@ import React, {
44 isValidElement ,
55 PropsWithChildren ,
66 HTMLAttributes ,
7+ useRef ,
8+ useEffect ,
79} from 'react'
810import { Slot } from '@radix-ui/react-slot'
911import { Icon } from '../Icon'
@@ -125,19 +127,85 @@ interface AppLayoutSidebarProps {
125127}
126128
127129const AppLayoutSidebar = ( { children, className } : AppLayoutSidebarProps ) => {
128- const { collapsed } = useAppLayout ( )
130+ const {
131+ collapsed,
132+ setCollapsed,
133+ hoverExpandsSidebar,
134+ _expandedByHover,
135+ _setExpandedByHover,
136+ } = useAppLayout ( )
129137
130138 const [ nav , rest ] = partitionBy ( Children . toArray ( children ) , ( child ) => {
131139 if ( ! isValidElement ( child ) ) return false
132140 const type = child . type as { displayName ?: string }
133141 return type . displayName === 'AppLayout.Nav'
134142 } )
135143
144+ const sidebarRef = useRef < HTMLDivElement > ( null )
145+ const hoverTimeoutRef = useRef < NodeJS . Timeout | null > ( null )
146+ const expandedByHoverRef = useRef ( _expandedByHover )
147+
148+ // Keep ref in sync with state
149+ useEffect ( ( ) => {
150+ expandedByHoverRef . current = _expandedByHover
151+ } , [ _expandedByHover ] )
152+
153+ // Handle hover intent when the sidebar is collapsed
154+ useEffect ( ( ) => {
155+ const sidebar = sidebarRef . current
156+ if ( ! sidebar || ! hoverExpandsSidebar ) return
157+
158+ const handleMouseEnter = ( ) => {
159+ // Only expand if currently collapsed
160+ if ( ! collapsed ) return
161+ // Clear any existing timeout
162+ if ( hoverTimeoutRef . current ) {
163+ clearTimeout ( hoverTimeoutRef . current )
164+ }
165+
166+ // Set a delay before expanding
167+ hoverTimeoutRef . current = setTimeout ( ( ) => {
168+ setCollapsed ( false )
169+ _setExpandedByHover ( true ) // Mark as expanded by hover
170+ hoverTimeoutRef . current = null
171+ } , 250 ) // 250ms delay
172+ }
173+
174+ const handleMouseLeave = ( ) => {
175+ // Clear the timeout if mouse leaves before delay completes
176+ if ( hoverTimeoutRef . current ) {
177+ clearTimeout ( hoverTimeoutRef . current )
178+ hoverTimeoutRef . current = null
179+ }
180+
181+ // Only collapse if it was expanded by hover, not manually
182+ if ( expandedByHoverRef . current ) {
183+ setCollapsed ( true )
184+ _setExpandedByHover ( false )
185+ }
186+ }
187+
188+ // Add listeners when hover expansion is enabled
189+ sidebar . addEventListener ( 'mouseenter' , handleMouseEnter )
190+ sidebar . addEventListener ( 'mouseleave' , handleMouseLeave )
191+
192+ // Cleanup function - always runs when effect re-runs or component unmounts
193+ return ( ) => {
194+ if ( hoverTimeoutRef . current ) {
195+ clearTimeout ( hoverTimeoutRef . current )
196+ hoverTimeoutRef . current = null
197+ }
198+ sidebar . removeEventListener ( 'mouseenter' , handleMouseEnter )
199+ sidebar . removeEventListener ( 'mouseleave' , handleMouseLeave )
200+ }
201+ } , [ hoverExpandsSidebar , collapsed , setCollapsed , _setExpandedByHover ] )
202+
136203 return (
137204 < motion . div
138205 initial = { false }
139206 layout = "position"
140207 className = { cn ( 'mt-4 flex w-fit flex-col items-start' , className ) }
208+ ref = { sidebarRef }
141209 transition = { { duration : 0.25 , type : 'spring' , bounce : 0 } }
142210 >
143211 < div className = "flex flex-col gap-4 px-2" >
@@ -276,7 +344,8 @@ interface AppLayoutCollapseButtonProps extends PropsWithChildren {
276344const AppLayoutCollapseButton = ( {
277345 className,
278346} : AppLayoutCollapseButtonProps ) => {
279- const { collapsed, setCollapsed, keybinds } = useAppLayout ( )
347+ const { collapsed, setCollapsed, keybinds, _setExpandedByHover } =
348+ useAppLayout ( )
280349 useAppLayoutKeys ( )
281350 return (
282351 < div className = { cn ( 'flex items-center gap-2' , className ) } >
@@ -285,7 +354,10 @@ const AppLayoutCollapseButton = ({
285354 < TooltipTrigger asChild >
286355 < button
287356 className = "group typography-body-md hover:bg-accent hover:text-primary rounded-md p-1.5"
288- onClick = { ( ) => setCollapsed ( ! collapsed ) }
357+ onClick = { ( ) => {
358+ setCollapsed ( ! collapsed )
359+ _setExpandedByHover ( false ) // Reset hover state on manual toggle
360+ } }
289361 aria-label = "Toggle sidebar"
290362 >
291363 < Icon
0 commit comments