{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "drawer", "title": "Drawer", "description": "A panel that slides in from any edge with a rough line on the exposed side.", "dependencies": [ "react-dom", "roughjs" ], "registryDependencies": [ "utils", "https://www.bydefaulthuman.fun/r/crumble-theme.json", "https://www.bydefaulthuman.fun/r/rough-lib.json" ], "files": [ { "path": "registry/new-york/ui/drawer.tsx", "content": "\"use client\";\n\nimport {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useRef,\n useState,\n type HTMLAttributes,\n type ReactNode,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { useRough } from \"@/hooks/use-rough\";\nimport { cn } from \"@/lib/utils\";\nimport { randomSeed, type CrumbleTheme } from \"@/lib/rough\";\n\n// ---------- context ----------\n\ninterface DrawerContextValue {\n onClose: () => void;\n open: boolean;\n side: DrawerSide;\n theme: CrumbleTheme;\n}\n\nconst DrawerContext = createContext({\n onClose: () => {},\n open: false,\n side: \"right\",\n theme: \"pencil\",\n});\n\n// ---------- types ----------\n\nexport type DrawerSide = \"left\" | \"right\" | \"top\" | \"bottom\";\n\nexport interface DrawerProps {\n children: ReactNode;\n onOpenChange?: (open: boolean) => void;\n open: boolean;\n side?: DrawerSide;\n theme?: CrumbleTheme;\n}\n\n// ---------- root ----------\n\nexport function Drawer({\n children,\n onOpenChange,\n open,\n side = \"right\",\n theme = \"pencil\",\n}: DrawerProps) {\n // Ref so the Escape handler always calls the latest onOpenChange\n // without needing to re-register the event listener on every render.\n const onOpenChangeRef = useRef(onOpenChange);\n useEffect(() => {\n onOpenChangeRef.current = onOpenChange;\n }, [onOpenChange]);\n\n const onClose = useCallback(() => onOpenChangeRef.current?.(false), []);\n\n useEffect(() => {\n if (!open) return;\n document.body.style.overflow = \"hidden\";\n return () => {\n document.body.style.overflow = \"\";\n };\n }, [open]);\n\n useEffect(() => {\n if (!open) return;\n const handler = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") onClose();\n };\n document.addEventListener(\"keydown\", handler);\n return () => document.removeEventListener(\"keydown\", handler);\n }, [open, onClose]);\n\n return (\n \n {children}\n \n );\n}\n\n// ---------- trigger ----------\n\nexport function DrawerTrigger({\n children,\n className,\n ...props\n}: HTMLAttributes) {\n return (\n \n {children}\n \n );\n}\n\n// ---------- content ----------\n\nexport interface DrawerContentProps extends HTMLAttributes {\n showClose?: boolean;\n size?: string;\n}\n\nconst roughEdge: Record = {\n bottom: \"top\",\n left: \"right\",\n right: \"left\",\n top: \"bottom\",\n};\n\nconst positionClass: Record = {\n bottom: \"bottom-0 left-0 right-0\",\n left: \"left-0 top-0 bottom-0\",\n right: \"right-0 top-0 bottom-0\",\n top: \"top-0 left-0 right-0\",\n};\n\nconst sizeStyle: Record React.CSSProperties> = {\n bottom: (s) => ({ height: s, width: \"100%\" }),\n left: (s) => ({ width: s, height: \"100%\" }),\n right: (s) => ({ width: s, height: \"100%\" }),\n top: (s) => ({ height: s, width: \"100%\" }),\n};\n\nexport function DrawerContent({\n children,\n className,\n showClose = true,\n size = \"320px\",\n ...props\n}: DrawerContentProps) {\n const { onClose, open, side, theme } = useContext(DrawerContext);\n const panelRef = useRef(null);\n const externalSvgRef = useRef(null);\n const { drawLine, svgRef } = useRough({\n variant: \"border\",\n svgRef: externalSvgRef,\n theme,\n });\n\n // SSR guard — portals can't render on the server\n const [mounted, setMounted] = useState(false);\n useEffect(() => {\n setMounted(true);\n }, []);\n\n const draw = useCallback(() => {\n const panel = panelRef.current;\n const svg = svgRef.current;\n if (!panel || !svg) return;\n\n svg.replaceChildren();\n\n const w = panel.offsetWidth;\n const h = panel.offsetHeight;\n const edge = roughEdge[side];\n\n const isVertical = edge === \"left\" || edge === \"right\";\n const svgW = isVertical ? 12 : w;\n const svgH = isVertical ? h : 12;\n\n svg.setAttribute(\"width\", String(svgW));\n svg.setAttribute(\"height\", String(svgH));\n svg.setAttribute(\"viewBox\", `0 0 ${svgW} ${svgH}`);\n\n const opts = {\n seed: randomSeed(),\n stroke: \"var(--cr-stroke)\",\n strokeWidth: theme === \"crayon\" ? 2.5 : theme === \"ink\" ? 1.5 : 1,\n };\n\n if (edge === \"left\") {\n const line = drawLine(6, 0, 6, h, opts);\n if (line) svg.appendChild(line);\n }\n if (edge === \"right\") {\n const line = drawLine(6, 0, 6, h, opts);\n if (line) svg.appendChild(line);\n }\n if (edge === \"top\") {\n const line = drawLine(0, 6, w, 6, opts);\n if (line) svg.appendChild(line);\n }\n if (edge === \"bottom\") {\n const line = drawLine(0, 6, w, 6, opts);\n if (line) svg.appendChild(line);\n }\n }, [drawLine, side, theme]);\n\n // Only draw when open — also fires when side/theme changes while open\n useEffect(() => {\n if (!open) return;\n const id = requestAnimationFrame(() => draw());\n return () => cancelAnimationFrame(id);\n }, [draw, open]);\n\n useEffect(() => {\n const panel = panelRef.current;\n if (!panel) return;\n const ro = new ResizeObserver(() => draw());\n ro.observe(panel);\n return () => ro.disconnect();\n }, [draw]);\n\n // Don't render on the server, and unmount the portal when closed\n if (!mounted || !open) return null;\n\n const edgeSvgPosition: Record<\n (typeof roughEdge)[DrawerSide],\n React.CSSProperties\n > = {\n bottom: { bottom: 0, left: 0, right: 0, height: 12 },\n left: { left: 0, top: 0, bottom: 0, width: 12 },\n right: { right: 0, top: 0, bottom: 0, width: 12 },\n top: { top: 0, left: 0, right: 0, height: 12 },\n };\n\n return createPortal(\n <>\n \n\n {/* Backdrop */}\n \n\n {/* Panel */}\n \n {/* Rough edge line */}\n \n\n {showClose ? (\n \n \n \n \n \n \n ) : null}\n\n {children}\n \n ,\n document.body,\n );\n}\n\n// ---------- slots ----------\n\nexport function DrawerHeader({\n children,\n className,\n ...props\n}: HTMLAttributes) {\n return (\n
\n {children}\n
\n );\n}\n\nexport function DrawerTitle({\n children,\n className,\n ...props\n}: HTMLAttributes) {\n return (\n \n {children}\n \n );\n}\n\nexport function DrawerDescription({\n children,\n className,\n ...props\n}: HTMLAttributes) {\n return (\n \n {children}\n

\n );\n}\n\nexport function DrawerBody({\n children,\n className,\n ...props\n}: HTMLAttributes) {\n return (\n
\n {children}\n
\n );\n}\n\nexport function DrawerFooter({\n children,\n className,\n ...props\n}: HTMLAttributes) {\n return (\n \n {children}\n \n );\n}\n", "type": "registry:ui", "target": "components/crumble/ui/drawer.tsx" } ], "type": "registry:ui" }