/* eslint-disable @typescript-eslint/no-explicit-any */ 'use client' import { AnimatePresence, motion } from 'framer-motion' import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-react' import * as React from 'react' import { Drawer, DrawerClose, DrawerContent, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger, } from '@/components/ui/drawer' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { cn } from '@/lib/utils' import { useSmallScreen } from '@/hooks/useMediaQuery' const ANIMATION_CONFIG = { variants: { enter: (direction: 'forward' | 'backward') => ({ x: direction === 'forward' ? '100%' : '-100%', opacity: 0, }), center: { x: 0, opacity: 1, }, exit: (direction: 'forward' | 'backward') => ({ x: direction === 'forward' ? '-100%' : '100%', opacity: 0, }), }, transition: { duration: 0.3, ease: [0.25, 0.1, 0.25, 1.0], }, } as const const getMobileItemStyles = ( isInsideGroup: boolean, inset?: boolean, variant?: string, disabled?: boolean ) => { return cn( 'flex cursor-pointer items-center justify-between px-4 py-4 w-full gap-4', !isInsideGroup && 'bg-main-view-fg/50 mx-2 my-1.5 rounded-md', isInsideGroup && 'bg-transparent py-4', inset && 'pl-8', variant === 'destructive' && 'text-destructive', disabled && 'pointer-events-none opacity-50' ) } const DropDrawerContext = React.createContext<{ isMobile: boolean }>({ isMobile: false, }) const useDropDrawerContext = () => { const context = React.useContext(DropDrawerContext) if (!context) { throw new Error( 'DropDrawer components cannot be rendered outside the DropDrawer Context' ) } return context } const useComponentSelection = () => { const { isMobile } = useDropDrawerContext() const selectComponent = (mobileComponent: T, desktopComponent: D) => { return isMobile ? mobileComponent : desktopComponent } return { isMobile, selectComponent } } const useGroupDetection = () => { const isInGroup = React.useCallback( (element: HTMLElement | null): boolean => { if (!element) return false let parent = element.parentElement while (parent) { if (parent.hasAttribute('data-drop-drawer-group')) { return true } parent = parent.parentElement } return false }, [] ) const useGroupState = () => { const { isMobile } = useComponentSelection() const itemRef = React.useRef(null) const [isInsideGroup, setIsInsideGroup] = React.useState(false) React.useEffect(() => { if (!isMobile) return const timer = setTimeout(() => { if (itemRef.current) { setIsInsideGroup(isInGroup(itemRef.current)) } }, 0) return () => clearTimeout(timer) }, [isMobile]) return { itemRef, isInsideGroup } } return { isInGroup, useGroupState } } type ConditionalComponentProps = { children: React.ReactNode className?: string } & (T | D) const ConditionalComponent = ({ mobileComponent, desktopComponent, children, ...props }: { mobileComponent: React.ComponentType desktopComponent: React.ComponentType children: React.ReactNode } & ConditionalComponentProps) => { const { selectComponent } = useComponentSelection() const Component = selectComponent(mobileComponent, desktopComponent) return {children} } function DropDrawer({ children, ...props }: | React.ComponentProps | React.ComponentProps) { const isMobile = useSmallScreen() return ( {children} ) } function DropDrawerTrigger({ className, children, ...props }: | React.ComponentProps | React.ComponentProps) { return ( {children} ) } function DropDrawerContent({ className, children, ...props }: | React.ComponentProps | React.ComponentProps) { const { isMobile } = useDropDrawerContext() const [activeSubmenu, setActiveSubmenu] = React.useState(null) const [submenuTitle, setSubmenuTitle] = React.useState(null) const [submenuStack, setSubmenuStack] = React.useState< { id: string; title: string }[] >([]) // Add animation direction state const [animationDirection, setAnimationDirection] = React.useState< 'forward' | 'backward' >('forward') // Create a ref to store submenu content by ID const submenuContentRef = React.useRef>( new Map() ) // Function to navigate to a submenu const navigateToSubmenu = React.useCallback((id: string, title: string) => { // Set animation direction to forward when navigating to a submenu setAnimationDirection('forward') setActiveSubmenu(id) setSubmenuTitle(title) setSubmenuStack((prev) => [...prev, { id, title }]) }, []) // Function to go back to previous menu const goBack = React.useCallback(() => { // Set animation direction to backward when going back setAnimationDirection('backward') if (submenuStack.length <= 1) { // If we're at the first level, go back to main menu setActiveSubmenu(null) setSubmenuTitle(null) setSubmenuStack([]) } else { // Go back to previous submenu const newStack = [...submenuStack] newStack.pop() // Remove current const previous = newStack[newStack.length - 1] setActiveSubmenu(previous.id) setSubmenuTitle(previous.title) setSubmenuStack(newStack) } }, [submenuStack]) // Function to register submenu content const registerSubmenuContent = React.useCallback( (id: string, content: React.ReactNode[]) => { submenuContentRef.current.set(id, content) }, [] ) const extractSubmenuContent = React.useCallback( (elements: React.ReactNode, targetId: string): React.ReactNode[] => { const result: React.ReactNode[] = [] const findSubmenuContent = (node: React.ReactNode) => { if (!React.isValidElement(node)) return const element = node as React.ReactElement const props = element.props as { 'id'?: string 'data-submenu-id'?: string 'children'?: React.ReactNode } if (element.type === DropDrawerSub) { const elementId = props.id || props['data-submenu-id'] if (elementId === targetId) { React.Children.forEach(props.children, (child) => { if ( React.isValidElement(child) && child.type === DropDrawerSubContent ) { const subContentProps = child.props as { children?: React.ReactNode } React.Children.forEach( subContentProps.children, (contentChild) => { result.push(contentChild) } ) } }) return } } if (props.children) { React.Children.forEach(props.children, findSubmenuContent) } } React.Children.forEach(elements, findSubmenuContent) return result }, [] ) // Get submenu content (always extract fresh to reflect state changes) const getSubmenuContent = React.useCallback( (id: string) => { // Always extract fresh content to ensure state changes are reflected const submenuContent = extractSubmenuContent(children, id) return submenuContent }, [children, extractSubmenuContent] ) if (isMobile) { return ( { if (id === null) { setActiveSubmenu(null) setSubmenuTitle(null) setSubmenuStack([]) } }, submenuTitle, setSubmenuTitle, navigateToSubmenu, registerSubmenuContent, }} > {activeSubmenu ? ( <>
{submenuTitle || 'Submenu'}
{/* Use AnimatePresence to handle exit animations */} {activeSubmenu ? getSubmenuContent(activeSubmenu) : children}
) : ( <> Menu
{children}
)}
) } return ( {children} ) } function DropDrawerItem({ className, children, onSelect, onClick, icon, variant = 'default', inset, disabled, ...props }: React.ComponentProps & { icon?: React.ReactNode }) { const { isMobile } = useComponentSelection() const { useGroupState } = useGroupDetection() const { itemRef, isInsideGroup } = useGroupState() if (isMobile) { const handleClick = (e: React.MouseEvent) => { if (disabled) return // If this item only has an icon (like a switch) and no other interactive content, // don't handle clicks on the main area - let the icon handle everything if (icon && !onClick && !onSelect) { return } // Check if the click came from the icon area (where the Switch is) const target = e.target as HTMLElement const iconContainer = (e.currentTarget as HTMLElement).querySelector( '[data-icon-container]' ) if (iconContainer && iconContainer.contains(target)) { // Don't handle the click if it came from the icon area return } if (onClick) onClick(e) if (onSelect) onSelect(e as unknown as Event) } // Only wrap in DrawerClose if it's not a submenu item const content = (
{children}
{icon && (
{icon}
)}
) // Check if this is inside a submenu const isInSubmenu = (props as Record)['data-parent-submenu-id'] || (props as Record)['data-parent-submenu'] if (isInSubmenu) { return content } return {content} } return ( } variant={variant} inset={inset} disabled={disabled} {...props} >
{children}
{icon &&
{icon}
}
) } function DropDrawerSeparator({ className, ...props }: React.ComponentProps) { const { isMobile } = useComponentSelection() if (isMobile) { return null } return ( ) } function DropDrawerLabel({ className, children, ...props }: | React.ComponentProps | React.ComponentProps) { const { isMobile } = useComponentSelection() if (isMobile) { return ( {children} ) } return ( {children} ) } function DropDrawerFooter({ className, children, ...props }: React.ComponentProps | React.ComponentProps<'div'>) { const { isMobile } = useDropDrawerContext() if (isMobile) { return ( {children} ) } // No direct equivalent in DropdownMenu, so we'll just render a div return (
{children}
) } function DropDrawerGroup({ className, children, ...props }: React.ComponentProps<'div'> & { children: React.ReactNode }) { const { isMobile } = useDropDrawerContext() // Add separators between children on mobile const childrenWithSeparators = React.useMemo(() => { if (!isMobile) return children const childArray = React.Children.toArray(children) // Filter out any existing separators const filteredChildren = childArray.filter( (child) => React.isValidElement(child) && child.type !== DropDrawerSeparator ) // Add separators between items return filteredChildren.flatMap((child, index) => { if (index === filteredChildren.length - 1) return [child] return [ child,