{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "sidebar", "title": "Sidebar", "description": "A collapsible sidebar component with customizable content and responsive design.", "dependencies": [ "@ark-ui/react", "lucide-react", "react-hotkeys-hook", "clsx", "tailwind-merge", "class-variance-authority" ], "files": [ { "path": "src/registry/thornberry/components/sidebar.tsx", "content": "import { ark } from \"@ark-ui/react/factory\";\nimport { TooltipContext } from \"@ark-ui/react/tooltip\";\nimport { PanelLeftIcon } from \"lucide-react\";\nimport {\n createContext,\n useCallback,\n useContext,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport { useHotkeys } from \"react-hotkeys-hook\";\n\nimport useIsMobile from \"@/lib/hooks/use-mobile\";\nimport { useSidebarResize } from \"@/lib/hooks/use-sidebar-resize\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/registry/thornberry/components/button\";\nimport { Input } from \"@/registry/thornberry/components/input\";\nimport {\n SheetContent,\n SheetDescription,\n SheetRoot,\n SheetTitle,\n SheetTrigger,\n} from \"@/registry/thornberry/components/sheet\";\nimport {\n TooltipContent,\n TooltipPositioner,\n TooltipRoot,\n TooltipTrigger,\n} from \"@/registry/thornberry/components/tooltip\";\n\nimport type { CSSProperties, ComponentProps } from \"react\";\n\nconst SIDEBAR_COOKIE_NAME = \"sidebar:state\";\nconst SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;\nconst SIDEBAR_WIDTH = \"16rem\";\nconst SIDEBAR_WIDTH_ICON = \"3rem\";\nconst MIN_SIDEBAR_WIDTH = \"14rem\";\nconst MAX_SIDEBAR_WIDTH = \"22rem\";\n\ninterface SidebarContextProps {\n state: \"expanded\" | \"collapsed\";\n open: boolean;\n setOpen: (open: boolean) => void;\n openMobile: boolean;\n setOpenMobile: (open: boolean) => void;\n isMobile: boolean;\n toggleSidebar: () => void;\n width: string;\n setWidth: (width: string) => void;\n isDraggingRail: boolean;\n setIsDraggingRail: (isDraggingRail: boolean) => void;\n}\n\nconst SidebarContext = createContext(null);\n\nconst useSidebar = () => {\n const context = useContext(SidebarContext);\n if (!context) {\n throw new Error(\"useSidebar must be used within a SidebarProvider.\");\n }\n\n return context;\n};\n\nconst SidebarProvider = ({\n defaultOpen = true,\n open: openProp,\n onOpenChange: setOpenProp,\n className,\n style,\n children,\n defaultWidth = SIDEBAR_WIDTH,\n ...rest\n}: ComponentProps<\"div\"> & {\n defaultOpen?: boolean;\n open?: boolean;\n onOpenChange?: (open: boolean) => void;\n defaultWidth?: string;\n}) => {\n const isMobile = useIsMobile();\n const [width, setWidth] = useState(defaultWidth);\n const [openMobile, setOpenMobile] = useState(false);\n const [isDraggingRail, setIsDraggingRail] = useState(false);\n\n // This is the internal state of the sidebar.\n // We use openProp and setOpenProp for control from outside the component.\n const [_open, _setOpen] = useState(defaultOpen);\n const open = openProp ?? _open;\n const setOpen = useCallback(\n (value: boolean | ((value: boolean) => boolean)) => {\n const openState = typeof value === \"function\" ? value(open) : value;\n if (setOpenProp) {\n setOpenProp(openState);\n } else {\n _setOpen(openState);\n }\n\n // This sets the cookie to keep the sidebar state.\n // biome-ignore lint/suspicious/noDocumentCookie: Boiler\n document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;\n },\n [setOpenProp, open],\n );\n\n // Helper to toggle the sidebar.\n const toggleSidebar = useCallback(() => {\n return isMobile ? setOpenMobile((open) => !open) : setOpen((open) => !open);\n }, [isMobile, setOpen]);\n\n // Adds a keyboard shortcut to toggle the sidebar.\n useHotkeys(\"b\", toggleSidebar, [toggleSidebar]);\n\n const state = open ? \"expanded\" : \"collapsed\";\n\n const contextValue = useMemo(\n () => ({\n state,\n open,\n setOpen,\n isMobile,\n openMobile,\n setOpenMobile,\n toggleSidebar,\n width,\n setWidth,\n isDraggingRail,\n setIsDraggingRail,\n }),\n [\n state,\n open,\n setOpen,\n isMobile,\n openMobile,\n toggleSidebar,\n width,\n isDraggingRail,\n ],\n );\n\n return (\n \n \n {children}\n \n \n );\n};\n\nconst Sidebar = ({\n side = \"left\",\n variant = \"sidebar\",\n collapsible = \"offcanvas\",\n className,\n children,\n ...rest\n}: ComponentProps<\"div\"> & {\n side?: \"left\" | \"right\";\n variant?: \"sidebar\" | \"floating\" | \"inset\";\n collapsible?: \"offcanvas\" | \"icon\" | \"none\";\n}) => {\n const { isMobile, state, openMobile, setOpenMobile, isDraggingRail } =\n useSidebar();\n\n if (collapsible === \"none\") {\n return (\n \n {children}\n \n );\n }\n\n if (isMobile) {\n return (\n setOpenMobile(open)}\n >\n \n \n \n button]:hidden\"\n side={side}\n >\n
\n Sidebar\n Displays the mobile sidebar.\n
\n
{children}
\n \n \n );\n }\n\n return (\n \n {/* This is what handles the sidebar gap on desktop */}\n \n \n \n {children}\n \n \n \n );\n};\n\nconst SidebarTrigger = ({\n className,\n onClick,\n ...rest\n}: ComponentProps) => {\n const { toggleSidebar } = useSidebar();\n\n return (\n {\n onClick?.(event);\n toggleSidebar();\n }}\n {...rest}\n >\n \n Toggle Sidebar\n \n );\n};\n\nconst SidebarRail = ({\n enableDrag = true,\n className,\n ...rest\n}: ComponentProps & {\n enableDrag?: boolean;\n}) => {\n const { toggleSidebar, setWidth, state, width, setIsDraggingRail } =\n useSidebar();\n\n const { dragRef, handleMouseDown } = useSidebarResize({\n onResize: setWidth,\n onToggle: toggleSidebar,\n currentWidth: width,\n isCollapsed: state === \"collapsed\",\n minResizeWidth: MIN_SIDEBAR_WIDTH,\n maxResizeWidth: MAX_SIDEBAR_WIDTH,\n setIsDraggingRail,\n widthCookieName: \"sidebar:width\",\n enableAutoCollapse: true,\n autoCollapseThreshold: 1.3,\n expandThreshold: 0.2,\n });\n\n const anchorRect = useRef(null);\n const getAnchorRect = useCallback(() => anchorRect.current, []);\n\n const isCollapsed = state === \"collapsed\";\n\n return (\n \n \n {(tootlip) => (\n {\n anchorRect.current = new DOMRect(e.clientX, e.clientY, 1, 1);\n tootlip.reposition();\n }}\n >\n \n\n \n \n Drag to resize\n
\n Click to {isCollapsed ? \"expand\" : \"collapse\"}{\" \"}\n
\n B\n
\n
\n
\n
\n \n );\n};\n\nconst SidebarInset = ({ className, ...rest }: ComponentProps<\"main\">) => {\n return (\n \n );\n};\n\nconst SidebarSeparator = ({ className, ...rest }: ComponentProps<\"div\">) => {\n return (\n \n );\n};\n\nconst SidebarInput = ({ className, ...rest }: ComponentProps) => {\n return (\n \n );\n};\n\nconst SidebarHeader = ({ className, ...rest }: ComponentProps<\"div\">) => {\n return (\n \n );\n};\n\nconst SidebarFooter = ({ className, ...rest }: ComponentProps<\"div\">) => {\n return (\n \n );\n};\n\nconst SidebarContent = ({ className, ...rest }: ComponentProps<\"div\">) => {\n return (\n \n );\n};\n\nconst SidebarGroup = ({ className, ...rest }: ComponentProps<\"div\">) => {\n return (\n \n );\n};\n\nconst SidebarGroupLabel = ({\n className,\n ...rest\n}: ComponentProps) => {\n return (\n svg]:size-4 [&>svg]:shrink-0\",\n \"group-data-[collapsible=icon]:hidden group-data-[collapsible=icon]:opacity-0\",\n className,\n )}\n {...rest}\n />\n );\n};\n\nconst SidebarGroupAction = ({\n className,\n ...rest\n}: ComponentProps) => {\n return (\n svg]:size-3 [&>svg]:shrink-0\",\n \"group-data-[collapsible=icon]:hidden\",\n className,\n )}\n {...rest}\n />\n );\n};\n\nconst SidebarGroupContent = ({ className, ...rest }: ComponentProps<\"div\">) => {\n return (\n \n );\n};\n\nconst SidebarMenu = ({ className, ...rest }: ComponentProps<\"div\">) => {\n return (\n \n );\n};\n\nconst SidebarMenuItem = ({ className, ...rest }: ComponentProps<\"div\">) => {\n return (\n \n );\n};\n\nconst SidebarMenuButton = ({\n isActive = false,\n tooltip,\n shortcut,\n className,\n ...rest\n}: ComponentProps & {\n isActive?: boolean;\n tooltip?: string;\n shortcut?: string;\n}) => {\n const { isMobile, state } = useSidebar();\n\n const button = (\n svg]:rotate-90\",\n // Typography and base text color\n \"font-medium text-sidebar-foreground/70\",\n // Hover styles\n \"hover:bg-sidebar-accent hover:text-sidebar-accent-foreground\",\n // Focus styles\n \"outline-hidden focus-visible:text-sidebar-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring! focus-visible:ring-offset-2! focus-visible:ring-offset-background\",\n // Active styles\n \"active:text-sidebar-accent-foreground\",\n // Disabled and aria-disabled styles\n \"disabled:pointer-events-none disabled:opacity-50\",\n \"aria-disabled:pointer-events-none aria-disabled:opacity-50\",\n // Data attribute states\n \"data-[active=true]:bg-sidebar-accent/80 data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground\",\n \"data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground\",\n // Collapsible size variant\n \"group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2!\",\n // Sidebar action padding adjustment\n \"group-has-data-[sidebar=menu-action]/menu-item:pr-8\",\n // Children styling\n \"[&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0\",\n className,\n )}\n {...rest}\n />\n );\n\n if (!tooltip) {\n return button;\n }\n\n return (\n \n {button}\n \n \n
\n {tooltip}\n {shortcut && (\n
\n \n {shortcut}\n \n
\n )}\n
\n
\n
\n \n );\n};\n\nconst SidebarMenuAction = ({\n isActive,\n className,\n showOnHover = false,\n ...rest\n}: ComponentProps & {\n isActive?: boolean;\n showOnHover?: boolean;\n}) => {\n return (\n svg]:size-4 [&>svg]:shrink-0\",\n \"after:absolute after:-inset-2 md:after:hidden\",\n \"peer-data-[size=sm]/menu-button:top-1\",\n \"peer-data-[size=default]/menu-button:top-1.5\",\n \"peer-data-[size=lg]/menu-button:top-2.5\",\n \"group-data-[collapsible=icon]:hidden\",\n showOnHover &&\n \"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0\",\n className,\n )}\n {...rest}\n />\n );\n};\n\nconst SidebarMenuBadge = ({ className, ...rest }: ComponentProps<\"div\">) => {\n return (\n \n );\n};\n\n// NB: Could extend on this with the hotkey hook for further advanced constality\nconst SidebarMenuShortcut = ({\n className,\n ...rest\n}: ComponentProps<\"span\">) => {\n return (\n \n );\n};\n\nconst SidebarMenuSub = ({ className, ...rest }: ComponentProps<\"ul\">) => {\n return (\n \n );\n};\n\nconst SidebarMenuSubItem = ({ className, ...rest }: ComponentProps<\"li\">) => {\n return (\n \n );\n};\n\nconst SidebarMenuSubButton = ({\n size = \"md\",\n isActive = false,\n className,\n ...rest\n}: ComponentProps & {\n size?: \"sm\" | \"md\";\n isActive?: boolean;\n}) => {\n return (\n span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground\",\n \"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground\",\n size === \"sm\" && \"text-xs\",\n size === \"md\" && \"text-sm\",\n \"group-data-[collapsible=icon]:hidden\",\n className,\n )}\n {...rest}\n />\n );\n};\n\nexport {\n Sidebar,\n SidebarContent,\n SidebarFooter,\n SidebarGroup,\n SidebarGroupAction,\n SidebarGroupContent,\n SidebarGroupLabel,\n SidebarHeader,\n SidebarInput,\n SidebarInset,\n SidebarMenu,\n SidebarMenuAction,\n SidebarMenuBadge,\n SidebarMenuButton,\n SidebarMenuItem,\n SidebarMenuSub,\n SidebarMenuSubButton,\n SidebarMenuSubItem,\n SidebarProvider,\n SidebarRail,\n SidebarTrigger,\n useSidebar,\n SidebarMenuShortcut,\n SidebarSeparator,\n};\n", "type": "registry:ui" }, { "path": "src/lib/hooks/use-mobile.ts", "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\nconst MOBILE_BREAKPOINT = 768;\n\nconst useIsMobile = () => {\n const [isMobile, setIsMobile] = useState(undefined);\n\n useEffect(() => {\n const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);\n const onChange = () => {\n setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);\n };\n mql.addEventListener(\"change\", onChange);\n setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);\n return () => mql.removeEventListener(\"change\", onChange);\n }, []);\n\n return !!isMobile;\n};\n\nexport default useIsMobile;\n", "type": "registry:lib", "target": "" }, { "path": "src/lib/hooks/use-sidebar-resize.ts", "content": "\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef } from \"react\";\n\nimport type { MouseEvent } from \"react\";\n\ninterface UseSidebarResizeProps {\n /**\n * Direction of the resize handle\n * - 'left': Handle is on left side (for right-positioned panels)\n * - 'right': Handle is on right side (for left-positioned panels)\n */\n direction?: \"left\" | \"right\";\n\n /**\n * Current width of the panel\n */\n currentWidth: string;\n\n /**\n * Callback to update width when resizing\n */\n onResize: (width: string) => void;\n\n /**\n * Callback to toggle panel visibility\n */\n onToggle?: () => void;\n\n /**\n * Whether the panel is currently collapsed\n */\n isCollapsed?: boolean;\n\n /**\n * Minimum resize width\n */\n minResizeWidth?: string;\n\n /**\n * Maximum resize width\n */\n maxResizeWidth?: string;\n\n /**\n * Whether to enable auto-collapse when dragged below threshold\n */\n enableAutoCollapse?: boolean;\n\n /**\n * Auto-collapse threshold as percentage of minResizeWidth\n * A value of 1.0 means the panel will collapse when dragged to minResizeWidth\n * A value of 0.5 means the panel will collapse when dragged to 50% of minResizeWidth\n * A value of 1.5 means the panel will collapse when dragged to 50% beyond minResizeWidth\n * Can be any positive number, not limited to the range 0.0-1.0\n */\n autoCollapseThreshold?: number;\n\n /**\n * Threshold to expand when dragging in opposite direction (0.0-1.0)\n * Percentage of distance needed to drag back to expand\n */\n expandThreshold?: number;\n\n /**\n * Whether to enable drag functionality\n */\n enableDrag?: boolean;\n\n /**\n * Callback to update dragging rail state\n */\n setIsDraggingRail?: (isDragging: boolean) => void;\n\n /**\n * Cookie name for persisting width\n */\n widthCookieName?: string;\n\n /**\n * Cookie max age in seconds\n */\n widthCookieMaxAge?: number;\n\n /**\n * Whether this is a nested sidebar (not at the edge of the screen)\n */\n isNested?: boolean;\n\n /**\n * Whether to enable toggle functionality\n */\n enableToggle?: boolean;\n}\n\ninterface WidthUnit {\n value: number;\n unit: \"rem\" | \"px\";\n}\n\n/**\n * Parse width string into value and unit\n */\nfunction parseWidth(width: string): WidthUnit {\n const unit = width.endsWith(\"rem\") ? \"rem\" : \"px\";\n const value = Number.parseFloat(width);\n return { value, unit };\n}\n\n/**\n * Convert any width to pixels for calculations\n */\nfunction toPx(width: string): number {\n const { value, unit } = parseWidth(width);\n return unit === \"rem\" ? value * 16 : value;\n}\n\n/**\n * Format width value with unit\n */\nfunction formatWidth(value: number, unit: \"rem\" | \"px\"): string {\n return `${unit === \"rem\" ? value.toFixed(1) : Math.round(value)}${unit}`;\n}\n\n/**\n * A versatile hook for handling resizable sidebar (or inset) panels\n * Works for both sidebar (left side) and artifacts (right side) panels\n * Supports VS Code-like continuous drag to collapse/expand\n */\nexport function useSidebarResize({\n direction = \"right\",\n currentWidth,\n onResize,\n onToggle,\n isCollapsed = false,\n minResizeWidth = \"14rem\",\n maxResizeWidth = \"24rem\",\n enableToggle = true,\n enableAutoCollapse = true,\n autoCollapseThreshold = 1.5, // Default to collapsing at minWidth + 50%\n expandThreshold = 0.2,\n enableDrag = true,\n setIsDraggingRail = () => {},\n widthCookieName,\n widthCookieMaxAge = 60 * 60 * 24 * 7, // 1 week default\n isNested = false,\n}: UseSidebarResizeProps) {\n // Refs for tracking drag state\n const dragRef = useRef(null);\n const startWidth = useRef(0);\n const startX = useRef(0);\n const isDragging = useRef(false);\n const isInteractingWithRail = useRef(false);\n const lastWidth = useRef(0);\n const lastLoggedWidth = useRef(0);\n const dragStartPoint = useRef(0);\n const lastDragDirection = useRef<\"expand\" | \"collapse\" | null>(null);\n const lastTogglePoint = useRef(0);\n const lastToggleWidth = useRef(0);\n const toggleCooldown = useRef(false);\n const lastToggleTime = useRef(0);\n const dragDistanceFromToggle = useRef(0);\n const dragOffset = useRef(0);\n const railRect = useRef(null);\n\n // Refs for auto-collapse threshold\n const autoCollapseThresholdPx = useRef(0);\n\n // Memoize min/max width calculations for performance\n const minWidthPx = useMemo(() => toPx(minResizeWidth), [minResizeWidth]);\n const maxWidthPx = useMemo(() => toPx(maxResizeWidth), [maxResizeWidth]);\n\n // Helper function to determine if width is increasing based on direction and mouse movement\n const isIncreasingWidth = useCallback(\n (currentX: number, referenceX: number): boolean => {\n return direction === \"left\"\n ? currentX < referenceX // For left-positioned handle, moving left increases width\n : currentX > referenceX; // For right-positioned handle, moving right increases width\n },\n [direction],\n );\n\n // Helper function to calculate width based on mouse position and direction\n const calculateWidth = useCallback(\n (\n e: MouseEvent,\n initialX: number,\n initialWidth: number,\n currentRailRect: DOMRect | null,\n ): number => {\n if (isNested && currentRailRect) {\n // For nested sidebars, use the delta from start position for precise tracking\n const deltaX = e.clientX - initialX;\n\n if (direction === \"left\") {\n // For left-positioned handle (right panel)\n // Width increases as mouse moves left (negative deltaX)\n return initialWidth - deltaX;\n }\n // For right-positioned handle (left panel)\n // Width increases as mouse moves right (positive deltaX)\n return initialWidth + deltaX;\n }\n // For standard sidebars at window edges\n if (direction === \"left\") {\n // For left-positioned handle (right panel)\n return window.innerWidth - e.clientX;\n }\n // For right-positioned handle (left panel)\n return e.clientX;\n },\n [direction, isNested],\n );\n\n // Update auto-collapse threshold when dependencies change\n useEffect(() => {\n autoCollapseThresholdPx.current = enableAutoCollapse\n ? minWidthPx * autoCollapseThreshold\n : 0;\n }, [minWidthPx, enableAutoCollapse, autoCollapseThreshold]);\n\n // Persist width to cookie if cookie name is provided\n const persistWidth = useCallback(\n (width: string) => {\n if (widthCookieName) {\n // biome-ignore lint/suspicious/noDocumentCookie: allow\n document.cookie = `${widthCookieName}=${width}; path=/; max-age=${widthCookieMaxAge}`;\n }\n },\n [widthCookieName, widthCookieMaxAge],\n );\n\n // Handle mouse down on resize handle\n const handleMouseDown = useCallback(\n (e: MouseEvent) => {\n isInteractingWithRail.current = true;\n\n if (!enableDrag) {\n return;\n }\n\n // Store initial state\n const currentWidthPx = isCollapsed ? 0 : toPx(currentWidth);\n startWidth.current = currentWidthPx;\n startX.current = e.clientX;\n dragStartPoint.current = e.clientX;\n lastWidth.current = currentWidthPx;\n lastLoggedWidth.current = currentWidthPx;\n lastTogglePoint.current = e.clientX;\n lastToggleWidth.current = currentWidthPx;\n lastDragDirection.current = null;\n toggleCooldown.current = false;\n lastToggleTime.current = 0;\n dragDistanceFromToggle.current = 0;\n\n // Reset drag offset\n dragOffset.current = 0;\n\n // Store the rail element's position for nested sidebars\n if (isNested && dragRef.current) {\n railRect.current = dragRef.current.getBoundingClientRect();\n } else {\n railRect.current = null;\n }\n\n e.preventDefault();\n },\n [enableDrag, isCollapsed, currentWidth, isNested],\n );\n\n // Handle mouse movement and resizing\n useEffect(() => {\n const handleMouseMove = (e: MouseEvent) => {\n if (!isInteractingWithRail.current) return;\n\n const deltaX = Math.abs(e.clientX - startX.current);\n if (!isDragging.current && deltaX > 5) {\n isDragging.current = true;\n setIsDraggingRail(true);\n }\n\n if (isDragging.current) {\n // Get unit for width calculations\n const { unit } = parseWidth(currentWidth);\n\n // Get current rail position for ultra-precise tracking\n let currentRailRect = railRect.current;\n if (isNested && dragRef.current) {\n currentRailRect = dragRef.current.getBoundingClientRect();\n }\n\n // Determine current drag direction\n const currentDragDirection = isIncreasingWidth(\n e.clientX,\n lastTogglePoint.current,\n )\n ? \"expand\"\n : \"collapse\";\n\n // Update direction tracking\n if (lastDragDirection.current !== currentDragDirection) {\n lastDragDirection.current = currentDragDirection;\n }\n\n // Calculate distance from last toggle point\n dragDistanceFromToggle.current = Math.abs(\n e.clientX - lastTogglePoint.current,\n );\n\n // Check for toggle cooldown (prevent rapid toggling)\n const now = Date.now();\n if (toggleCooldown.current && now - lastToggleTime.current > 200) {\n toggleCooldown.current = false;\n }\n\n // Handle toggling between collapsed and expanded states\n if (!toggleCooldown.current) {\n // Handle collapsing when expanded\n if (enableAutoCollapse && onToggle && !isCollapsed) {\n // Calculate precise width based on mouse position\n const currentDragWidth = calculateWidth(\n e,\n startX.current,\n startWidth.current,\n currentRailRect,\n );\n\n // Determine if we should collapse based on threshold\n let shouldCollapse = false;\n\n if (autoCollapseThreshold <= 1.0) {\n // For thresholds <= 1.0, collapse when width is below minWidth * threshold\n shouldCollapse =\n currentDragWidth <= minWidthPx * autoCollapseThreshold;\n } else {\n // For thresholds > 1.0, we need to drag beyond minWidth by a certain amount\n if (currentDragWidth <= minWidthPx) {\n // Calculate how much beyond minWidth we need to drag\n const extraDragNeeded =\n minWidthPx * (autoCollapseThreshold - 1.0);\n\n // Only collapse if we've dragged far enough beyond minWidth\n const distanceBeyondMin = minWidthPx - currentDragWidth;\n\n shouldCollapse = distanceBeyondMin >= extraDragNeeded;\n }\n }\n\n if (currentDragDirection === \"collapse\" && shouldCollapse) {\n onToggle(); // Collapse\n lastTogglePoint.current = e.clientX;\n lastToggleWidth.current = 0; // Width is 0 when collapsed\n toggleCooldown.current = true;\n lastToggleTime.current = now;\n return;\n }\n }\n\n // Handle expanding when collapsed\n if (\n onToggle &&\n isCollapsed &&\n currentDragDirection === \"expand\" &&\n dragDistanceFromToggle.current > minWidthPx * expandThreshold\n ) {\n onToggle(); // Expand\n\n // Calculate initial width based on exact mouse position\n const initialWidth = calculateWidth(\n e,\n startX.current,\n startWidth.current,\n currentRailRect,\n );\n\n // Clamp to min/max\n const clampedWidth = Math.max(\n minWidthPx,\n Math.min(maxWidthPx, initialWidth),\n );\n\n // Set initial width when expanding\n const formattedWidth = formatWidth(\n unit === \"rem\" ? clampedWidth / 16 : clampedWidth,\n unit,\n );\n onResize(formattedWidth);\n persistWidth(formattedWidth);\n\n lastTogglePoint.current = e.clientX;\n lastToggleWidth.current = clampedWidth;\n toggleCooldown.current = true;\n lastToggleTime.current = now;\n return;\n }\n }\n\n // Skip width calculations if panel is collapsed\n if (isCollapsed) {\n return;\n }\n\n // Calculate new width based on mouse position and drag direction\n const newWidthPx = calculateWidth(\n e,\n startX.current,\n startWidth.current,\n currentRailRect,\n );\n\n // Clamp width between min and max\n const clampedWidthPx = Math.max(\n minWidthPx,\n Math.min(maxWidthPx, newWidthPx),\n );\n\n // Convert to the target unit\n const newWidth = unit === \"rem\" ? clampedWidthPx / 16 : clampedWidthPx;\n\n // Format and update width\n const formattedWidth = formatWidth(newWidth, unit);\n onResize(formattedWidth);\n persistWidth(formattedWidth);\n\n // Update last width\n lastWidth.current = clampedWidthPx;\n }\n };\n\n const handleMouseUp = () => {\n if (!isInteractingWithRail.current) return;\n\n // Handle click (not drag) behavior\n if (!isDragging.current && onToggle && enableToggle) {\n onToggle();\n }\n\n // Reset all state\n isDragging.current = false;\n isInteractingWithRail.current = false;\n lastWidth.current = 0;\n lastLoggedWidth.current = 0;\n lastDragDirection.current = null;\n lastTogglePoint.current = 0;\n lastToggleWidth.current = 0;\n toggleCooldown.current = false;\n lastToggleTime.current = 0;\n dragDistanceFromToggle.current = 0;\n dragOffset.current = 0;\n railRect.current = null;\n setIsDraggingRail(false);\n };\n\n // @ts-expect-error\n document.addEventListener(\"mousemove\", handleMouseMove);\n document.addEventListener(\"mouseup\", handleMouseUp);\n\n return () => {\n // @ts-expect-error\n document.removeEventListener(\"mousemove\", handleMouseMove);\n document.removeEventListener(\"mouseup\", handleMouseUp);\n };\n }, [\n onResize,\n onToggle,\n isCollapsed,\n currentWidth,\n persistWidth,\n setIsDraggingRail,\n minWidthPx,\n maxWidthPx,\n isIncreasingWidth,\n calculateWidth,\n isNested,\n enableAutoCollapse,\n autoCollapseThreshold,\n expandThreshold,\n enableToggle,\n ]);\n\n return {\n dragRef,\n isDragging,\n handleMouseDown,\n };\n}\n", "type": "registry:lib", "target": "" }, { "path": "src/lib/utils.ts", "content": "import { clsx } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nimport type { ClassValue } from \"clsx\";\n\nexport const cn = (...inputs: ClassValue[]) => {\n return twMerge(clsx(inputs));\n};\n", "type": "registry:lib", "target": "" }, { "path": "src/registry/thornberry/components/button.tsx", "content": "import { ark } from \"@ark-ui/react\";\nimport { cva } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\n\nimport type { VariantProps } from \"class-variance-authority\";\nimport type { ComponentProps } from \"react\";\n\nconst buttonVariants = cva(\n \"inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 whitespace-nowrap outline-none focus-visible:outline-none focus-visible:ring-offset-background focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 rounded-md font-medium text-sm outline-hidden disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0 transition-[color,box-shadow,transform]\",\n {\n variants: {\n variant: {\n solid:\n \"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90\",\n outline:\n \"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground\",\n muted: \"bg-muted text-muted-foreground shadow-xs hover:bg-muted/80\",\n ghost:\n \"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50\",\n link: \"text-primary underline-offset-2 hover:underline\",\n destructive:\n \"bg-destructive text-background shadow-xs hover:bg-destructive/90 focus-visible:ring-red-500 aria-invalid:ring-red-500/20 dark:aria-invalid:ring-red-500/40\",\n },\n size: {\n sm: \"h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5\",\n md: \"h-9 px-4 py-2 has-[>svg]:px-3\",\n lg: \"h-10 rounded-md px-6 has-[>svg]:px-4\",\n icon: \"size-9\",\n },\n },\n defaultVariants: {\n variant: \"solid\",\n size: \"md\",\n },\n },\n);\n\nconst Button = ({\n className,\n variant,\n size,\n ...rest\n}: ComponentProps & VariantProps) => (\n \n);\n\nexport { Button, buttonVariants };\n", "type": "registry:component", "target": "" }, { "path": "src/registry/thornberry/components/input.tsx", "content": "import { ark } from \"@ark-ui/react\";\n\nimport { cn } from \"@/lib/utils\";\n\nimport type { ComponentProps } from \"react\";\n\n// TODO: add label\nconst Input = ({ className, ...rest }: ComponentProps) => (\n \n);\n\nexport { Input };\n", "type": "registry:component", "target": "" }, { "path": "src/registry/thornberry/components/sheet.tsx", "content": "import { Dialog as ArkSheet } from \"@ark-ui/react/dialog\";\nimport { X } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nimport type { ComponentProps } from \"react\";\n\nconst SheetProvider = ArkSheet.RootProvider;\nconst SheetContext = ArkSheet.Context;\nconst SheetRoot = ArkSheet.Root;\n\nconst SheetTrigger = ({\n className,\n ...rest\n}: ComponentProps) => (\n \n);\n\nconst SheetBackdrop = ({\n className,\n ...rest\n}: ComponentProps) => (\n \n);\n\nconst SheetPositioner = ({\n className,\n ...rest\n}: ComponentProps) => (\n \n);\n\ninterface ArkSheetContentProps extends ComponentProps {\n side?: \"top\" | \"right\" | \"bottom\" | \"left\";\n}\n\nconst SheetContent = ({\n className,\n side = \"left\",\n ...rest\n}: ArkSheetContentProps) => (\n \n);\n\nconst SheetTitle = ({\n className,\n ...rest\n}: ComponentProps) => (\n \n);\n\nconst SheetDescription = ({\n className,\n ...rest\n}: ComponentProps) => (\n \n);\n\nconst SheetCloseTrigger = ({\n className,\n children,\n asChild,\n ...rest\n}: ComponentProps) => {\n // If there are no children, render the default X icon\n if (!children) {\n return (\n \n \n Close\n \n );\n }\n\n // If children are provided, use them inside the CloseTrigger\n // This is useful for creating buttons that close the ArkSheet\n return (\n \n {children}\n \n );\n};\n\nexport {\n SheetRoot,\n SheetTrigger,\n SheetBackdrop,\n SheetPositioner,\n SheetContent,\n SheetTitle,\n SheetDescription,\n SheetCloseTrigger,\n SheetProvider,\n SheetContext,\n};\n", "type": "registry:component", "target": "" }, { "path": "src/registry/thornberry/components/tooltip.tsx", "content": "import { Tooltip as ArkTooltip } from \"@ark-ui/react/tooltip\";\n\nimport { cn } from \"@/lib/utils\";\n\nimport type { ComponentProps } from \"react\";\n\nconst TooltipProvider = ArkTooltip.RootProvider;\nconst TooltipRoot = ArkTooltip.Root;\nconst TooltipArrow = ArkTooltip.Arrow;\n\nconst TooltipTrigger = ({\n className,\n ...rest\n}: ComponentProps) => (\n \n);\n\nconst TooltipPositioner = ({\n className,\n ...rest\n}: ComponentProps) => (\n \n);\n\nconst TooltipContent = ({\n className,\n ...rest\n}: ComponentProps) => (\n \n);\n\nconst TooltipArrowTip = ({\n className,\n ...rest\n}: ComponentProps) => (\n \n);\n\nexport {\n TooltipArrow,\n TooltipArrowTip,\n TooltipContent,\n TooltipPositioner,\n TooltipProvider,\n TooltipRoot,\n TooltipTrigger,\n};\n", "type": "registry:component", "target": "" } ], "type": "registry:ui" }