"use client"; import * as React from "react"; import { ChevronRight, ChevronLeft } from "lucide-react"; import { motion, useInView, HTMLMotionProps } from "framer-motion"; import { twMerge } from "tailwind-merge"; import clsx from "clsx"; // Re-implementing the 'cn' utility function directly for self-containment function cn(...inputs: clsx.ClassValue[]) { return twMerge(clsx(inputs)); } interface SidebarContextType { expanded: boolean; onChange: (expanded: boolean) => void; activeMenuItem: string | null; setActiveMenuItem: (id: string | null) => void; menuItemPosition: React.MutableRefObject<{ left: number; width: number; top: number; height: number; }>; menuItemRefs: React.MutableRefObject>; menuRef: React.RefObject; updateIndicatorPosition: (id: string | null) => void; // New: Function to notify provider when a menu item ref is added/removed notifyMenuItemRefChange: () => void; } const SidebarContext = React.createContext( undefined ); interface SidebarProviderProps { defaultExpanded?: boolean; expanded?: boolean; onExpandedChange?: (expanded: boolean) => void; children: React.ReactNode; } export function SidebarProvider({ defaultExpanded = true, expanded: controlledExpanded, onExpandedChange, children, }: SidebarProviderProps) { const [expanded, setExpanded] = React.useState(defaultExpanded); const [activeMenuItem, setActiveMenuItem] = React.useState( null ); const menuItemPosition = React.useRef({ left: 0, width: 0, top: 0, height: 0, }); const menuItemRefs = React.useRef>( new Map() ); const menuRef = React.useRef(null); // NEW: State to force re-evaluation when menuItemRefs content might have changed const [menuRefsVersion, setMenuRefsVersion] = React.useState(0); const isControlled = controlledExpanded !== undefined; const actualExpanded = isControlled ? controlledExpanded : expanded; const onExpandedChangeRef = React.useRef(onExpandedChange); React.useEffect(() => { onExpandedChangeRef.current = onExpandedChange; }, [onExpandedChange]); const handleExpandedChange = React.useCallback( (value: boolean) => { if (!isControlled) { setExpanded(value); } onExpandedChangeRef.current?.(value); }, [isControlled] ); // NEW: Callback to increment the version when a menu item ref is added/removed const notifyMenuItemRefChange = React.useCallback(() => { setMenuRefsVersion((prev) => prev + 1); }, []); // Helper function to encapsulate indicator positioning logic const updateIndicatorPosition = React.useCallback( (id: string | null) => { const indicator = menuRef.current?.querySelector( ".sidebar-menu-indicator" ) as HTMLElement | null; if (id && menuRef.current) { const selectedItem = menuItemRefs.current.get(id); if (selectedItem) { const menuRect = menuRef.current.getBoundingClientRect(); const rect = selectedItem.getBoundingClientRect(); menuItemPosition.current = { left: rect.left - menuRect.left, width: rect.width, top: rect.top - menuRect.top, height: rect.height, }; if (indicator) { indicator.style.left = `${menuItemPosition.current.left}px`; indicator.style.width = `${menuItemPosition.current.width}px`; indicator.style.top = `${menuItemPosition.current.top}px`; indicator.style.height = `${menuItemPosition.current.height}px`; indicator.style.opacity = "1"; } } else { // If selectedItem is not found (e.g., not yet mounted or invalid ID) // Ensure the indicator is hidden until the item is ready if (indicator) { indicator.style.opacity = "0"; } } } else { // If no active ID, hide the indicator if (indicator) { indicator.style.opacity = "0"; } } }, [menuItemRefs, menuRef, menuItemPosition] ); // Effect to set active menu item from URL React.useEffect(() => { const url = new URL(window.location.href); const searchParams = url.searchParams; const path = url.pathname; let potentialMenuItemValue: string | null = null; if (searchParams.has("component")) { potentialMenuItemValue = searchParams.get("component"); } else { const pathSegments = path.split("/").filter((segment) => segment); if (pathSegments.length > 0) { potentialMenuItemValue = pathSegments[pathSegments.length - 1]; } } setActiveMenuItem(potentialMenuItemValue); // No need to call updateIndicatorPosition directly here. // The useLayoutEffect below, which depends on menuRefsVersion, will handle it. }, [window.location.pathname, window.location.search]); // Primary useLayoutEffect for synchronous indicator updates React.useLayoutEffect(() => { // This effect runs whenever activeMenuItem changes OR when menuRefsVersion increments. // By depending on menuRefsVersion, we ensure that if an item registers its ref // AFTER activeMenuItem is set (e.g., on initial load/navigation), // this effect will // re-run and find the newly available ref. updateIndicatorPosition(activeMenuItem); }, [activeMenuItem, menuRefsVersion, menuRef, updateIndicatorPosition]); // Effect to re-adjust on window resize/layout changes React.useEffect(() => { const handleResize = () => { if (activeMenuItem) { updateIndicatorPosition(activeMenuItem); } }; window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); }, [activeMenuItem, updateIndicatorPosition]); return ( {children} ); } export function useSidebar() { const context = React.useContext(SidebarContext); if (!context) { throw new Error("useSidebar must be used within a SidebarProvider"); } return context; } interface SidebarProps extends React.HTMLAttributes {} export function Sidebar({ className, children, ...props }: SidebarProps) { const { expanded } = useSidebar(); return (
{children}
); } interface SidebarTriggerProps extends React.ButtonHTMLAttributes {} export function SidebarTrigger({ className, ...props }: SidebarTriggerProps) { const { expanded, onChange } = useSidebar(); return ( ); } interface SidebarHeaderProps extends React.HTMLAttributes {} export function SidebarHeader({ className, children, ...props }: SidebarHeaderProps) { const { expanded } = useSidebar(); return (
{children}
); } interface SidebarContentProps extends React.HTMLAttributes {} export function SidebarContent({ className, children, ...props }: SidebarContentProps) { const scrollRef = React.useRef(null); return (
{children}
); } interface SidebarGroupProps extends React.HTMLAttributes {} export function SidebarGroup({ className, children, ...props }: SidebarGroupProps) { return (
{children}
); } interface SidebarGroupLabelProps extends React.HTMLAttributes {} export function SidebarGroupLabel({ className, children, ...props }: SidebarGroupLabelProps) { const { expanded } = useSidebar(); if (!expanded) { return null; } return (
{children}
); } interface SidebarGroupContentProps extends React.HTMLAttributes {} export function SidebarGroupContent({ className, children, ...props }: SidebarGroupContentProps) { return (
{children}
); } interface SidebarFooterProps extends React.HTMLAttributes {} export function SidebarFooter({ className, children, ...props }: SidebarFooterProps) { const { expanded } = useSidebar(); return (
{children}
); } interface SidebarMenuProps extends React.HTMLAttributes {} export function SidebarMenu({ className, children, ...props }: SidebarMenuProps) { const { menuRef } = useSidebar(); return ( // In your SidebarMenu component's div for the indicator:
{" "} {/* Removed border classes */} {children}
); } // FIX: Omit conflicting framer-motion props from HTMLDivElement attributes interface SidebarMenuItemProps extends Omit< React.HTMLAttributes, | "onDrag" | "onDragStart" | "onDragEnd" | "onAnimationStart" | "onAnimationEnd" > { value?: string; } export function SidebarMenuItem({ className, children, value, ...props }: SidebarMenuItemProps) { const itemRef = React.useRef(null); // NEW: Get notifyMenuItemRefChange from context const { activeMenuItem, menuItemRefs, notifyMenuItemRefChange } = useSidebar(); const menuItemId = value || React.useId(); const isActive = activeMenuItem === menuItemId; const isInView = useInView(itemRef, { once: false, amount: 0.5 }); // Register this menu item when it mounts // and NOTIFY the provider about the change React.useEffect(() => { if (itemRef.current) { menuItemRefs.current.set(menuItemId, itemRef.current); // Notify the provider that a ref has been added, potentially triggering // the useLayoutEffect if this item is the active one. notifyMenuItemRefChange(); } return () => { menuItemRefs.current.delete(menuItemId); // Also notify when a ref is removed (component unmounts) notifyMenuItemRefChange(); }; }, [menuItemRefs, menuItemId, notifyMenuItemRefChange]); // Added notifyMenuItemRefChange to deps return ( )} > {children} ); } interface SidebarMenuButtonProps extends React.HTMLAttributes { asChild?: boolean; value?: string; } export function SidebarMenuButton({ className, children, asChild = false, value, ...props }: SidebarMenuButtonProps) { const { expanded, activeMenuItem, setActiveMenuItem, updateIndicatorPosition, } = useSidebar(); const menuItemId = value || React.useId(); const isActive = activeMenuItem === menuItemId; const handleClick = React.useCallback(() => { setActiveMenuItem(menuItemId); // Explicitly call updateIndicatorPosition immediately on click. // This provides immediate visual feedback for direct clicks, overriding // any potential slight delay from the useLayoutEffect waiting for version update. updateIndicatorPosition(menuItemId); if (props.onClick && typeof props.onClick === "function") { const dummyEvent = { currentTarget: {} as EventTarget & HTMLDivElement, target: {} as EventTarget, preventDefault: () => {}, stopPropagation: () => {}, } as React.MouseEvent; props.onClick(dummyEvent); } }, [menuItemId, setActiveMenuItem, updateIndicatorPosition, props.onClick]); const sharedClassName = "flex cursor-pointer items-center rounded-md px-3 py-2 text-sm hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring "; if (!expanded) { if (asChild) { return (
{React.Children.map(children, (child) => { if (React.isValidElement(child)) { return React.cloneElement(child, { ...child.props, className: cn( sharedClassName, "justify-center p-2", "hover:bg-primary/10 hover:scale-110", isActive ? "text-primary font-medium" : "", child.props?.className ), }); } return child; })}
); } return (
{React.Children.toArray(children).filter( (child) => React.isValidElement(child) && typeof child.type !== "string" )}
); } if (asChild) { return (
{React.Children.map(children, (child) => { if (React.isValidElement(child)) { return React.cloneElement(child, { ...child.props, className: cn( sharedClassName, "justify-start gap-2", "hover:bg-primary/10 hover:translate-x-1", isActive ? "text-primary font-medium" : "", child.props?.className ), }); } return child; })}
); } return (
{children}
); } export { Sidebar as SidebarRoot };