{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "sidebar-context", "type": "registry:ui", "title": "Sidebar Context", "description": "SidebarGlass Context", "dependencies": [], "registryDependencies": [], "files": [ { "path": "components/glass/ui/sidebar-context.tsx", "type": "registry:component", "content": "/* eslint-disable react-refresh/only-export-components */\n/**\n * SidebarGlass Context\n *\n * Provides state management for SidebarGlass compound component.\n * 100% compatible with shadcn/ui Sidebar API.\n *\n * @module sidebar-context\n */\n\nimport {\n createContext,\n useContext,\n useState,\n useCallback,\n useMemo,\n useEffect,\n type FC,\n type ReactNode,\n} from 'react';\n\n// ========================================\n// TYPES\n// ========================================\n\nexport type SidebarSide = 'left' | 'right';\nexport type SidebarVariant = 'sidebar' | 'floating' | 'inset';\nexport type SidebarCollapsible = 'offcanvas' | 'icon' | 'none';\n\n/**\n * Context value for SidebarGlass compound components\n * 100% compatible with shadcn/ui useSidebar() hook\n */\nexport interface SidebarContextValue {\n /** Sidebar state: \"expanded\" | \"collapsed\" */\n state: 'expanded' | 'collapsed';\n /** Whether sidebar is open (desktop) */\n open: boolean;\n /** Set sidebar open state */\n setOpen: (open: boolean) => void;\n /** Whether mobile drawer is open */\n openMobile: boolean;\n /** Set mobile drawer open state */\n setOpenMobile: (open: boolean) => void;\n /** Whether viewport is mobile */\n isMobile: boolean;\n /** Toggle sidebar open/close */\n toggleSidebar: () => void;\n\n // === CONFIG ===\n /** Which side the sidebar is on */\n side: SidebarSide;\n /** Sidebar variant */\n variant: SidebarVariant;\n /** Collapsible mode */\n collapsible: SidebarCollapsible;\n}\n\n// ========================================\n// CONSTANTS\n// ========================================\n\nconst MOBILE_BREAKPOINT = 768;\nconst SIDEBAR_COOKIE_NAME = 'sidebar:state';\nconst SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; // 7 days\n\n// ========================================\n// CONTEXT\n// ========================================\n\nconst SidebarContext = createContext(null);\n\n/**\n * Hook to access Sidebar context (100% shadcn compatible)\n *\n * @throws Error if used outside of SidebarGlass.Provider\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * const { state, open, toggleSidebar, isMobile } = useSidebar();\n *\n * return (\n * \n * );\n * }\n * ```\n */\nexport function useSidebar(): SidebarContextValue {\n const context = useContext(SidebarContext);\n if (!context) {\n throw new Error(\n 'useSidebar must be used within SidebarGlass.Provider. ' +\n 'Wrap your component tree with .'\n );\n }\n return context;\n}\n\n/**\n * Optional hook that returns null if outside provider (doesn't throw)\n */\nexport function useSidebarOptional(): SidebarContextValue | null {\n return useContext(SidebarContext);\n}\n\n// ========================================\n// PROVIDER\n// ========================================\n\n/**\n * Props for SidebarGlass.Provider (100% shadcn compatible)\n */\nexport interface SidebarProviderProps {\n children: ReactNode;\n\n /** Controlled open state */\n open?: boolean;\n /** Callback when open state changes */\n onOpenChange?: (open: boolean) => void;\n /** Default open state (uncontrolled) @default true */\n defaultOpen?: boolean;\n\n /** Which side the sidebar is on @default \"left\" */\n side?: SidebarSide;\n /** Sidebar variant @default \"sidebar\" */\n variant?: SidebarVariant;\n /** Collapsible mode @default \"offcanvas\" */\n collapsible?: SidebarCollapsible;\n\n /** Cookie name for persistence @default \"sidebar:state\" */\n cookieName?: string;\n /** Keyboard shortcut key (Cmd/Ctrl + key) @default \"b\" */\n keyboardShortcut?: string | false;\n}\n\n/**\n * Provider component for SidebarGlass compound components\n *\n * @example\n * ```tsx\n * \n * \n * \n * \n * ...\n * \n * \n * \n * \n *
Main content
\n *
\n *
\n * ```\n */\nexport const SidebarProvider: FC = ({\n children,\n open: controlledOpen,\n onOpenChange,\n defaultOpen = true,\n side = 'left',\n variant = 'sidebar',\n collapsible = 'offcanvas',\n cookieName = SIDEBAR_COOKIE_NAME,\n keyboardShortcut = 'b',\n}) => {\n // === OPEN STATE (controlled/uncontrolled with cookie persistence) ===\n const [internalOpen, setInternalOpen] = useState(() => {\n // Try to read from cookie\n if (typeof document !== 'undefined') {\n const cookies = document.cookie.split(';');\n const sidebarCookie = cookies.find((c) => c.trim().startsWith(`${cookieName}=`));\n if (sidebarCookie) {\n return sidebarCookie.split('=')[1] === 'true';\n }\n }\n return defaultOpen;\n });\n\n const isControlled = controlledOpen !== undefined;\n const open = isControlled ? controlledOpen : internalOpen;\n\n const setOpen = useCallback(\n (value: boolean) => {\n if (!isControlled) {\n setInternalOpen(value);\n }\n onOpenChange?.(value);\n\n // Persist to cookie\n if (typeof document !== 'undefined') {\n document.cookie = `${cookieName}=${value}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;\n }\n },\n [isControlled, onOpenChange, cookieName]\n );\n\n // === MOBILE STATE ===\n const [openMobile, setOpenMobile] = useState(false);\n\n // === RESPONSIVE DETECTION ===\n const [isMobile, setIsMobile] = useState(() => {\n if (typeof window === 'undefined') return false;\n return window.innerWidth < MOBILE_BREAKPOINT;\n });\n\n useEffect(() => {\n if (typeof window === 'undefined') return;\n\n const checkMobile = () => {\n const mobile = window.innerWidth < MOBILE_BREAKPOINT;\n setIsMobile(mobile);\n // Close mobile drawer when switching to desktop (in same callback)\n if (!mobile) {\n setOpenMobile(false);\n }\n };\n\n checkMobile();\n window.addEventListener('resize', checkMobile);\n return () => window.removeEventListener('resize', checkMobile);\n }, []);\n\n // === TOGGLE ACTION ===\n const toggleSidebar = useCallback(() => {\n if (isMobile) {\n setOpenMobile((prev) => !prev);\n } else {\n setOpen(!open);\n }\n }, [isMobile, open, setOpen]);\n\n // === KEYBOARD NAVIGATION ===\n useEffect(() => {\n if (!keyboardShortcut) return;\n\n const handleKeyDown = (e: KeyboardEvent) => {\n // Cmd/Ctrl + key - toggle sidebar\n if (e.key === keyboardShortcut && (e.metaKey || e.ctrlKey)) {\n e.preventDefault();\n toggleSidebar();\n }\n };\n\n document.addEventListener('keydown', handleKeyDown);\n return () => document.removeEventListener('keydown', handleKeyDown);\n }, [keyboardShortcut, toggleSidebar]);\n\n // === CONTEXT VALUE ===\n const value = useMemo(\n () => ({\n state: open ? 'expanded' : 'collapsed',\n open,\n setOpen,\n openMobile,\n setOpenMobile,\n isMobile,\n toggleSidebar,\n side,\n variant,\n collapsible,\n }),\n [open, setOpen, openMobile, isMobile, toggleSidebar, side, variant, collapsible]\n );\n\n return {children};\n};\n\nSidebarProvider.displayName = 'SidebarGlass.Provider';\n" } ], "categories": [ "ui" ], "cssVars": { "light": { "--glass-bg": "rgba(255, 255, 255, 0.1)", "--glass-border": "rgba(255, 255, 255, 0.2)", "--blur-sm": "8px", "--blur-md": "16px", "--blur-lg": "24px" }, "dark": { "--glass-bg": "rgba(255, 255, 255, 0.05)", "--glass-border": "rgba(255, 255, 255, 0.1)", "--blur-sm": "8px", "--blur-md": "16px", "--blur-lg": "24px" } } }