{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "drawer",
  "type": "registry:ui",
  "title": "Drawer",
  "description": "Drawer overlay for Tile UI.",
  "dependencies": [
    "@radix-ui/react-slot",
    "react-dom"
  ],
  "devDependencies": [
    "sass"
  ],
  "registryDependencies": [
    "@tile-ui/core",
    "@tile-ui/portal",
    "@tile-ui/styles"
  ],
  "files": [
    {
      "path": "packages/react/src/components/drawer/drawer.tsx",
      "type": "registry:ui",
      "target": "components/ui/drawer/drawer.tsx",
      "content": "import React, { createContext, useCallback, useContext, useEffect, useId, useLayoutEffect, useRef, useState } from 'react';\nimport { createPortal } from 'react-dom';\nimport { Slot } from '@radix-ui/react-slot';\nimport { drawerStyleKeys, getDrawerState, getDrawerTranslateStyle } from '../lib/core';\nimport type { DrawerBaseProps, DrawerDirection } from '../lib/core';\nimport { usePortalContainer, type PortalContainer } from '../portal';\nimport styles from './drawer.module.scss';\n\ninterface DrawerContextValue {\n\topen: boolean;\n\tsetOpen: (open: boolean) => void;\n\tclose: () => void;\n\tdirection: DrawerDirection;\n\tmodal: boolean;\n\ttitleId: string;\n\tdescriptionId: string;\n}\n\nconst DrawerContext = createContext<DrawerContextValue | null>(null);\n\nfunction useDrawerContext(): DrawerContextValue {\n\tconst context = useContext(DrawerContext);\n\tif (!context) {\n\t\tthrow new Error('Drawer sub-components must be used within <Drawer>.');\n\t}\n\treturn context;\n}\n\nexport interface DrawerProps extends DrawerBaseProps {\n\topen?: boolean;\n\tdefaultOpen?: boolean;\n\tonOpenChange?: (open: boolean) => void;\n\tchildren?: React.ReactNode;\n}\n\nfunction Drawer({ direction = 'right', modal = true, open, defaultOpen = false, onOpenChange, children }: DrawerProps) {\n\tconst [internalOpen, setInternalOpen] = useState(defaultOpen);\n\tconst onOpenChangeRef = useRef(onOpenChange);\n\n\tuseEffect(() => {\n\t\tonOpenChangeRef.current = onOpenChange;\n\t});\n\n\tconst setOpen = useCallback(\n\t\t(next: boolean) => {\n\t\t\tif (open === undefined) {\n\t\t\t\tsetInternalOpen(next);\n\t\t\t}\n\n\t\t\tonOpenChangeRef.current?.(next);\n\t\t},\n\t\t[open],\n\t);\n\n\tconst close = useCallback(() => {\n\t\tsetOpen(false);\n\t}, [setOpen]);\n\n\tconst isOpen = open !== undefined ? open : internalOpen;\n\tconst titleId = useId();\n\tconst descriptionId = useId();\n\n\treturn <DrawerContext.Provider value={{ open: isOpen, setOpen, close, direction, modal, titleId, descriptionId }}>{children}</DrawerContext.Provider>;\n}\n\nDrawer.displayName = 'Drawer';\n\nexport interface DrawerTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n\tasChild?: boolean;\n}\n\nconst DrawerTrigger = React.forwardRef<HTMLButtonElement, DrawerTriggerProps>(({ asChild = false, className = '', children, onClick, ...props }, ref) => {\n\tconst context = useDrawerContext();\n\tconst Comp = asChild ? Slot : 'button';\n\n\treturn (\n\t\t<Comp\n\t\t\tref={ref}\n\t\t\ttype={asChild ? undefined : 'button'}\n\t\t\tclassName={className}\n\t\t\tonClick={(event: React.MouseEvent<HTMLButtonElement>) => {\n\t\t\t\tonClick?.(event);\n\t\t\t\tcontext.setOpen(true);\n\t\t\t}}\n\t\t\t{...props}>\n\t\t\t{children}\n\t\t</Comp>\n\t);\n});\nDrawerTrigger.displayName = 'DrawerTrigger';\n\nexport interface DrawerCloseProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\n\tasChild?: boolean;\n}\n\nconst DrawerClose = React.forwardRef<HTMLButtonElement, DrawerCloseProps>(({ asChild = false, className = '', children, onClick, ...props }, ref) => {\n\tconst context = useDrawerContext();\n\tconst Comp = asChild ? Slot : 'button';\n\n\treturn (\n\t\t<Comp\n\t\t\tref={ref}\n\t\t\ttype={asChild ? undefined : 'button'}\n\t\t\tclassName={className}\n\t\t\tonClick={(event: React.MouseEvent<HTMLButtonElement>) => {\n\t\t\t\tonClick?.(event);\n\t\t\t\tcontext.close();\n\t\t\t}}\n\t\t\t{...props}>\n\t\t\t{children}\n\t\t</Comp>\n\t);\n});\nDrawerClose.displayName = 'DrawerClose';\n\nexport interface DrawerOverlayProps extends React.HTMLAttributes<HTMLDivElement> {}\n\nconst DrawerOverlay = React.forwardRef<HTMLDivElement, DrawerOverlayProps>(({ className = '', onClick, ...props }, ref) => {\n\tconst context = useDrawerContext();\n\n\tif (!context.open || !context.modal) {\n\t\treturn null;\n\t}\n\n\treturn (\n\t\t<div\n\t\t\tref={ref}\n\t\t\tdata-state={getDrawerState(context.open)}\n\t\t\tclassName={`${styles[drawerStyleKeys.overlay]} ${className}`}\n\t\t\tonClick={(event: React.MouseEvent<HTMLDivElement>) => {\n\t\t\t\tonClick?.(event);\n\t\t\t\tcontext.close();\n\t\t\t}}\n\t\t\t{...props}\n\t\t/>\n\t);\n});\nDrawerOverlay.displayName = 'DrawerOverlay';\n\nexport interface DrawerContentProps extends React.HTMLAttributes<HTMLDivElement> {\n\tshowCloseButton?: boolean;\n\tcontainer?: PortalContainer;\n}\n\nconst DrawerContent = React.forwardRef<HTMLDivElement, DrawerContentProps>(({ className = '', showCloseButton = true, container, style, children, ...props }, ref) => {\n\tconst { open, close, direction, modal, titleId, descriptionId } = useDrawerContext();\n\tconst portalContainer = usePortalContainer(container);\n\tconst contentRef = useRef<HTMLDivElement | null>(null);\n\tconst [isVisible, setIsVisible] = useState(false);\n\n\tfunction setContentRef(element: HTMLDivElement | null) {\n\t\tcontentRef.current = element;\n\n\t\tif (typeof ref === 'function') {\n\t\t\tref(element);\n\t\t} else if (ref) {\n\t\t\tref.current = element;\n\t\t}\n\t}\n\n\tuseLayoutEffect(() => {\n\t\tif (!open) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst previouslyFocused = document.activeElement as HTMLElement | null;\n\t\tconst previousOverflow = document.body.style.overflow;\n\t\tif (modal) {\n\t\t\tdocument.body.style.overflow = 'hidden';\n\t\t}\n\n\t\tconst handleKeyDown = (event: KeyboardEvent) => {\n\t\t\tif (event.key === 'Escape' && modal) {\n\t\t\t\tclose();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (event.key === 'Tab' && modal) {\n\t\t\t\tconst container = contentRef.current;\n\t\t\t\tif (!container) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst focusables = Array.from(\n\t\t\t\t\tcontainer.querySelectorAll<HTMLElement>(\n\t\t\t\t\t\t'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])',\n\t\t\t\t\t),\n\t\t\t\t).filter((el) => el.getClientRects().length > 0);\n\t\t\t\tif (focusables.length === 0) {\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tconst first = focusables[0];\n\t\t\t\tconst last = focusables[focusables.length - 1];\n\t\t\t\tconst active = document.activeElement;\n\n\t\t\t\tif (event.shiftKey) {\n\t\t\t\t\tif (active === first || active === container || !container.contains(active)) {\n\t\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\t\tlast.focus();\n\t\t\t\t\t}\n\t\t\t\t} else if (active === last || !container.contains(active)) {\n\t\t\t\t\tevent.preventDefault();\n\t\t\t\t\tfirst.focus();\n\t\t\t\t}\n\t\t\t}\n\t\t};\n\n\t\tdocument.addEventListener('keydown', handleKeyDown);\n\n\t\tconst contentEl = contentRef.current;\n\t\tif (contentEl) {\n\t\t\tcontentEl.focus();\n\t\t}\n\n\t\treturn () => {\n\t\t\tdocument.body.style.overflow = previousOverflow;\n\t\t\tdocument.removeEventListener('keydown', handleKeyDown);\n\t\t\tpreviouslyFocused?.focus();\n\t\t};\n\t}, [close, open, modal]);\n\n\tuseEffect(() => {\n\t\tif (!open) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst frame = requestAnimationFrame(() => {\n\t\t\tsetIsVisible(true);\n\t\t});\n\n\t\treturn () => cancelAnimationFrame(frame);\n\t}, [open]);\n\n\tif (!open || !portalContainer) {\n\t\treturn null;\n\t}\n\n\treturn createPortal(\n\t\t<>\n\t\t\t{modal && <DrawerOverlay />}\n\t\t\t<div\n\t\t\t\tref={setContentRef}\n\t\t\t\trole=\"dialog\"\n\t\t\t\taria-modal={modal ? 'true' : 'false'}\n\t\t\t\taria-labelledby={titleId}\n\t\t\t\taria-describedby={descriptionId}\n\t\t\t\ttabIndex={-1}\n\t\t\t\tdata-state={getDrawerState(open)}\n\t\t\t\tdata-direction={direction}\n\t\t\t\tstyle={{ transform: isVisible ? '' : getDrawerTranslateStyle(direction), opacity: isVisible ? '' : 0, ...style }}\n\t\t\t\tclassName={`${styles[drawerStyleKeys.content]} ${className}`}\n\t\t\t\t{...props}>\n\t\t\t\t<div className={styles[drawerStyleKeys.handle]} />\n\t\t\t\t{children}\n\t\t\t\t{showCloseButton && (\n\t\t\t\t\t<button type=\"button\" aria-label=\"关闭\" className={styles[drawerStyleKeys.close]} onClick={() => close()}>\n\t\t\t\t\t\t<svg\n\t\t\t\t\t\t\tclassName={styles[drawerStyleKeys.xIcon]}\n\t\t\t\t\t\t\txmlns=\"http://www.w3.org/2000/svg\"\n\t\t\t\t\t\t\twidth=\"16\"\n\t\t\t\t\t\t\theight=\"16\"\n\t\t\t\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\t\t\t\tfill=\"none\"\n\t\t\t\t\t\t\tstroke=\"currentColor\"\n\t\t\t\t\t\t\tstrokeWidth=\"2\"\n\t\t\t\t\t\t\tstrokeLinecap=\"round\"\n\t\t\t\t\t\t\tstrokeLinejoin=\"round\"\n\t\t\t\t\t\t\taria-hidden=\"true\">\n\t\t\t\t\t\t\t<path d=\"M18 6 6 18\" />\n\t\t\t\t\t\t\t<path d=\"m6 6 12 12\" />\n\t\t\t\t\t\t</svg>\n\t\t\t\t\t</button>\n\t\t\t\t)}\n\t\t\t</div>\n\t\t</>,\n\t\tportalContainer,\n\t);\n});\nDrawerContent.displayName = 'DrawerContent';\n\nexport interface DrawerHeaderProps extends React.HTMLAttributes<HTMLDivElement> {}\n\nconst DrawerHeader = React.forwardRef<HTMLDivElement, DrawerHeaderProps>(({ className = '', children, ...props }, ref) => {\n\treturn (\n\t\t<div ref={ref} className={`${styles[drawerStyleKeys.header]} ${className}`} {...props}>\n\t\t\t{children}\n\t\t</div>\n\t);\n});\nDrawerHeader.displayName = 'DrawerHeader';\n\nexport interface DrawerFooterProps extends React.HTMLAttributes<HTMLDivElement> {}\n\nconst DrawerFooter = React.forwardRef<HTMLDivElement, DrawerFooterProps>(({ className = '', children, ...props }, ref) => {\n\treturn (\n\t\t<div ref={ref} className={`${styles[drawerStyleKeys.footer]} ${className}`} {...props}>\n\t\t\t{children}\n\t\t</div>\n\t);\n});\nDrawerFooter.displayName = 'DrawerFooter';\n\nexport interface DrawerTitleProps extends React.HTMLAttributes<HTMLHeadingElement> {}\n\nconst DrawerTitle = React.forwardRef<HTMLHeadingElement, DrawerTitleProps>(({ className = '', children, ...props }, ref) => {\n\tconst context = useDrawerContext();\n\n\treturn (\n\t\t<h2 ref={ref} id={context.titleId} className={`${styles[drawerStyleKeys.title]} ${className}`} {...props}>\n\t\t\t{children}\n\t\t</h2>\n\t);\n});\nDrawerTitle.displayName = 'DrawerTitle';\n\nexport interface DrawerDescriptionProps extends React.HTMLAttributes<HTMLParagraphElement> {}\n\nconst DrawerDescription = React.forwardRef<HTMLParagraphElement, DrawerDescriptionProps>(({ className = '', children, ...props }, ref) => {\n\tconst context = useDrawerContext();\n\n\treturn (\n\t\t<p ref={ref} id={context.descriptionId} className={`${styles[drawerStyleKeys.description]} ${className}`} {...props}>\n\t\t\t{children}\n\t\t</p>\n\t);\n});\nDrawerDescription.displayName = 'DrawerDescription';\n\nexport { Drawer, DrawerTrigger, DrawerClose, DrawerOverlay, DrawerContent, DrawerHeader, DrawerFooter, DrawerTitle, DrawerDescription };\nexport default Drawer;\n"
    },
    {
      "path": "packages/styles/scss/components/drawer.module.scss",
      "type": "registry:file",
      "target": "components/ui/drawer/drawer.module.scss",
      "content": "@use '../../../styles/variables/colors' as *;\n\n// 半透明遮罩\n.overlay {\n\tposition: fixed;\n\tinset: 0;\n\tz-index: $z-modal;\n\tbackground-color: var(--tile-drawer-overlay, $overlay);\n\n\t&[data-state='open'] {\n\t\tanimation: drawer-overlay-in $transition-normal;\n\t}\n\n\t&[data-state='closed'] {\n\t\topacity: 0;\n\t}\n}\n\n@keyframes drawer-overlay-in {\n\tfrom {\n\t\topacity: 0;\n\t}\n\n\tto {\n\t\topacity: 1;\n\t}\n}\n\n// 抽屉内容面板\n.content {\n\tposition: fixed;\n\tz-index: $z-modal;\n\tdisplay: flex;\n\tflex-direction: column;\n\tbackground-color: var(--tile-drawer-background, $card);\n\tcolor: $card-foreground;\n\tbox-shadow: var(--tile-drawer-shadow, $shadow-lg);\n\toutline: none;\n\ttransition:\n\t\ttransform $transition-slow,\n\t\topacity $transition-fast;\n\n\t&[data-direction='right'] {\n\t\tinset: 0 0 0 auto;\n\t\twidth: 75%;\n\t\theight: 100%;\n\t\tmax-width: 24rem;\n\t\tborder-left: 1px solid var(--tile-drawer-border, $border);\n\t}\n\n\t&[data-direction='left'] {\n\t\tinset: 0 auto 0 0;\n\t\twidth: 75%;\n\t\theight: 100%;\n\t\tmax-width: 24rem;\n\t\tborder-right: 1px solid var(--tile-drawer-border, $border);\n\t}\n\n\t&[data-direction='top'] {\n\t\tinset: 0 0 auto 0;\n\t\twidth: 100%;\n\t\tmax-height: 80vh;\n\t\tborder-bottom: 1px solid var(--tile-drawer-border, $border);\n\t}\n\n\t&[data-direction='bottom'] {\n\t\tinset: auto 0 0 0;\n\t\twidth: 100%;\n\t\tmax-height: 80vh;\n\t\tborder-top: 1px solid var(--tile-drawer-border, $border);\n\t}\n}\n\n// 顶部 / 底部方向的手柄\n.handle {\n\tmargin: $spacing-4 auto 0;\n\tdisplay: none;\n\theight: 0.5rem;\n\twidth: 100px;\n\tflex-shrink: 0;\n\tborder-radius: $radius-full;\n\tbackground-color: var(--tile-drawer-handle-background, $muted);\n}\n\n.content[data-direction='top'] .handle,\n.content[data-direction='bottom'] .handle {\n\tdisplay: block;\n}\n\n// 右上角关闭按钮\n.close {\n\tposition: absolute;\n\ttop: $spacing-4;\n\tright: $spacing-4;\n\tdisplay: inline-flex;\n\talign-items: center;\n\tjustify-content: center;\n\tpadding: $spacing-1;\n\tborder: none;\n\tborder-radius: $radius;\n\tbackground-color: transparent;\n\tcolor: $muted-foreground;\n\topacity: 0.7;\n\tcursor: pointer;\n\ttransition: opacity $transition-fast;\n\toutline: none;\n\n\t&:hover {\n\t\topacity: 1;\n\t}\n\n\t&:focus-visible {\n\t\tbox-shadow: var(--tile-drawer-close-focus-shadow, 0 0 0 2px $background, 0 0 0 4px $ring);\n\t}\n\n\t.xIcon {\n\t\twidth: 1rem;\n\t\theight: 1rem;\n\t\tpointer-events: none;\n\t}\n}\n\n.header {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: $spacing-1_5;\n\tpadding: var(--tile-drawer-header-padding, $spacing-4);\n}\n\n.footer {\n\tmargin-top: auto;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: $spacing-2;\n\tpadding: var(--tile-drawer-footer-padding, $spacing-4);\n}\n\n.title {\n\tmargin: 0;\n\tfont-size: var(--tile-drawer-title-size, $text-base);\n\tfont-weight: $font-semibold;\n\tline-height: var(--tile-drawer-title-leading, $leading-none);\n\tcolor: $foreground;\n}\n\n.description {\n\tmargin: 0;\n\tfont-size: $text-sm;\n\tline-height: $leading-normal;\n\tcolor: var(--tile-drawer-description-color, $muted-foreground);\n}\n"
    },
    {
      "path": "packages/react/src/components/drawer/index.ts",
      "type": "registry:ui",
      "target": "components/ui/drawer/index.ts",
      "content": "export {\n\tDrawer,\n\tDrawerTrigger,\n\tDrawerClose,\n\tDrawerOverlay,\n\tDrawerContent,\n\tDrawerHeader,\n\tDrawerFooter,\n\tDrawerTitle,\n\tDrawerDescription,\n\ttype DrawerProps,\n\ttype DrawerTriggerProps,\n\ttype DrawerCloseProps,\n\ttype DrawerOverlayProps,\n\ttype DrawerContentProps,\n\ttype DrawerHeaderProps,\n\ttype DrawerFooterProps,\n\ttype DrawerTitleProps,\n\ttype DrawerDescriptionProps,\n} from './drawer';\n"
    }
  ]
}
