{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "masai-drawer", "title": "Masai Drawer", "description": "Controlled drawer component with bottom/right/left directions and swipe-close behavior on mobile.", "dependencies": [ "vaul", "lucide-react" ], "files": [ { "path": "registry/components/masai-drawer.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Drawer } from \"vaul\";\nimport { X } from \"lucide-react\";\n\nimport { MasaiButton } from \"@/registry/components/masai-button\";\nimport { cn } from \"@/lib/utils\";\n\ntype DrawerDirection = \"bottom\" | \"right\" | \"left\";\n\ntype MasaiDrawerProps = {\n isOpen: boolean;\n onOpenChange: (isOpen: boolean) => void;\n content: React.ReactNode | React.ComponentType;\n direction?: DrawerDirection;\n sideMarginInPx?: number;\n title?: React.ReactNode;\n showCloseButton?: boolean;\n className?: string;\n overlayClassName?: string;\n};\n\nfunction resolveContent(content: MasaiDrawerProps[\"content\"]) {\n if (React.isValidElement(content)) return content;\n if (typeof content === \"function\") {\n const Content = content as React.ComponentType;\n return ;\n }\n return content;\n}\n\nconst drawerDirectionClassNames: Record = {\n bottom: \"left-0 right-0 bottom-0 max-h-[88svh] rounded-t-2xl border-t\",\n right: \"right-0 top-0 h-svh w-[420px] max-w-full border-l\",\n left: \"left-0 top-0 h-svh w-[420px] max-w-full border-r\",\n};\n\nexport function MasaiDrawer({\n isOpen,\n onOpenChange,\n content,\n direction = \"bottom\",\n sideMarginInPx,\n title,\n showCloseButton = true,\n className,\n overlayClassName,\n}: MasaiDrawerProps) {\n const renderedContent = React.useMemo(() => resolveContent(content), [content]);\n const hasFloatingMargin = typeof sideMarginInPx === \"number\" && sideMarginInPx > 0;\n const floatingPanelStyle = React.useMemo(() => {\n if (!hasFloatingMargin || !sideMarginInPx) return undefined;\n\n const spacing = `${sideMarginInPx}px`;\n const viewportInsetWidth = `calc(100vw - ${sideMarginInPx * 2}px)`;\n const viewportInsetHeight = `calc(100svh - ${sideMarginInPx * 2}px)`;\n\n if (direction === \"right\") {\n return {\n top: spacing,\n right: spacing,\n bottom: spacing,\n width: `min(420px, ${viewportInsetWidth})`,\n height: viewportInsetHeight,\n } as React.CSSProperties;\n }\n\n if (direction === \"left\") {\n return {\n top: spacing,\n left: spacing,\n bottom: spacing,\n width: `min(420px, ${viewportInsetWidth})`,\n height: viewportInsetHeight,\n } as React.CSSProperties;\n }\n\n return {\n top: spacing,\n left: spacing,\n right: spacing,\n bottom: spacing,\n height: viewportInsetHeight,\n } as React.CSSProperties;\n }, [direction, hasFloatingMargin, sideMarginInPx]);\n\n return (\n \n \n onOpenChange(false)}\n className={cn(\n \"fixed inset-0 z-50 bg-black/50 transition-opacity duration-300 ease-out data-[state=closed]:opacity-0 data-[state=open]:opacity-100\",\n overlayClassName,\n )}\n />\n \n onOpenChange(false)}\n />\n \n {direction === \"bottom\" ? (\n
\n ) : null}\n\n {title || showCloseButton ? (\n
\n \n {title ?? \"Drawer\"}\n \n {showCloseButton ? (\n }\n htmlType=\"button\"\n onClick={() => onOpenChange(false)}\n aria-label=\"Close drawer\"\n className=\"!h-8 !w-8 !rounded-md !border !border-slate-200 !text-slate-500 hover:!bg-slate-50 hover:!text-slate-800\"\n />\n ) : null}\n
\n ) : null}\n\n
{renderedContent}
\n
\n
\n
\n \n );\n}\n\nexport type { DrawerDirection, MasaiDrawerProps };\n", "type": "registry:ui" } ], "type": "registry:ui" }