{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "mobile-drawer",
  "title": "Mobile Drawer",
  "description": "A bottom-sheet drawer with swipe-to-dismiss, focus trap, and scroll lock.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "components/ui/mobile-drawer.tsx",
      "type": "registry:ui",
      "content": "'use client'\n\nimport {\n  forwardRef,\n  useCallback,\n  useEffect,\n  useId,\n  useRef,\n  useState,\n  type HTMLAttributes,\n  type ReactNode,\n} from 'react'\nimport { createPortal } from 'react-dom'\nimport {\n  AnimatePresence,\n  motion,\n  useMotionValue,\n  useReducedMotion,\n  useTransform,\n} from 'motion/react'\nimport { cn } from '@/lib/utils'\nimport { springs } from '@/lib/motion-tokens'\nimport { useFocusTrap } from '@/lib/hooks/use-focus-trap'\nimport { useMounted } from '@/hooks/use-mounted'\n\nexport interface MobileDrawerProps extends HTMLAttributes<HTMLDivElement> {\n  open: boolean\n  onClose: () => void\n  children: ReactNode\n  triggerRef?: React.RefObject<HTMLElement | null>\n  dismissible?: boolean\n}\n\nexport interface MobileDrawerTitleProps extends HTMLAttributes<HTMLHeadingElement> {\n  children: ReactNode\n}\n\nexport const MobileDrawerTitle = forwardRef<HTMLHeadingElement, MobileDrawerTitleProps>(\n  ({ children, className, ...props }, ref) => (\n    <h2\n      ref={ref}\n      className={cn('px-6 pt-6 pb-2 text-lg font-semibold text-(--color-fg)', className)}\n      {...props}\n    >\n      {children}\n    </h2>\n  ),\n)\nMobileDrawerTitle.displayName = 'MobileDrawerTitle'\n\nexport const MobileDrawer = forwardRef<HTMLDivElement, MobileDrawerProps>(\n  (\n    {\n      open,\n      onClose,\n      children,\n      triggerRef: triggerRefProp,\n      dismissible = true,\n      className,\n      ...props\n    },\n    ref,\n  ) => {\n    const reduceMotion = useReducedMotion()\n    const mounted = useMounted()\n    const panelRef = useRef<HTMLDivElement>(null)\n    const dialogRef = useRef<HTMLDivElement>(null)\n    const previousScrollRef = useRef<string>('')\n    const dragY = useMotionValue(0)\n    const overlayOpacity = useTransform(dragY, [0, 300], [0.8, 0])\n\n    const reactId = useId()\n    const titleId = `${reactId}-drawer-title`\n\n    useFocusTrap(dialogRef, open, triggerRefProp)\n\n    useEffect(() => {\n      if (!open) return\n      previousScrollRef.current = document.body.style.overflow\n      document.body.style.overflow = 'hidden'\n      return () => {\n        document.body.style.overflow = previousScrollRef.current\n      }\n    }, [open])\n\n    const handleDragEnd = useCallback(\n      (_: unknown, info: { offset: { y: number }; velocity: { y: number } }) => {\n        if (!dismissible) return\n        const panelHeight = panelRef.current?.offsetHeight ?? 300\n        const pastThreshold = info.offset.y > panelHeight * 0.3\n        const fastFlick = info.velocity.y > 500\n        if (pastThreshold || fastFlick) {\n          onClose()\n        }\n      },\n      [dismissible, onClose],\n    )\n\n    if (!mounted) return null\n\n    return createPortal(\n      <AnimatePresence>\n        {open && (\n          <div\n            key=\"mobile-drawer\"\n            ref={(node) => {\n              ;(dialogRef as React.MutableRefObject<HTMLDivElement | null>).current = node\n              if (typeof ref === 'function') ref(node)\n              else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node\n            }}\n            className=\"fixed inset-0 z-300\"\n            role=\"dialog\"\n            aria-modal=\"true\"\n            aria-labelledby={titleId}\n            {...props}\n          >\n            <motion.div\n              className=\"absolute inset-0 bg-(--color-bg)\"\n              style={{ opacity: reduceMotion ? 0.8 : overlayOpacity }}\n              initial={{ opacity: 0 }}\n              animate={{ opacity: reduceMotion ? 0.8 : 0.8 }}\n              exit={{ opacity: 0 }}\n              transition={{ duration: reduceMotion ? 0 : 0.2 }}\n              onClick={dismissible ? onClose : undefined}\n              aria-hidden=\"true\"\n            />\n\n            <motion.div\n              ref={panelRef}\n              className={cn(\n                'absolute inset-x-0 bottom-0 max-h-[85dvh] overflow-y-auto rounded-t-lg squircle-corners bg-(--color-bg) pb-[env(safe-area-inset-bottom)]',\n                className,\n              )}\n              style={{\n                y: dragY,\n              }}\n              initial={reduceMotion ? { y: 0 } : { y: '100%' }}\n              animate={reduceMotion ? { y: 0 } : { y: 0 }}\n              exit={reduceMotion ? { y: 0 } : { y: '100%' }}\n              transition={reduceMotion ? { duration: 0 } : springs.settle}\n              drag={dismissible ? 'y' : false}\n              dragConstraints={{ top: 0 }}\n              dragElastic={0.1}\n              onDragEnd={handleDragEnd}\n            >\n              <div className=\"flex justify-center pt-3 pb-2\" aria-hidden=\"true\">\n                <div className=\"h-1 w-9 rounded-full bg-(--color-border)\" />\n              </div>\n\n              <div data-title-id={titleId}>{children}</div>\n            </motion.div>\n          </div>\n        )}\n      </AnimatePresence>,\n      document.body,\n    )\n  },\n)\nMobileDrawer.displayName = 'MobileDrawer'\n\nexport function MobileDrawerPreview() {\n  const [open, setOpen] = useState(false)\n  return (\n    <div\n      className=\"flex h-full w-full items-center justify-center p-6\"\n      style={{ backgroundColor: 'var(--color-surface)' }}\n    >\n      <button\n        type=\"button\"\n        onClick={() => setOpen(true)}\n        className=\"rounded-lg squircle-corners border border-(--color-border) bg-(--color-surface) px-4 py-2.5 text-sm text-(--color-fg)\"\n      >\n        Open Drawer\n      </button>\n      <MobileDrawer open={open} onClose={() => setOpen(false)} aria-label=\"Filters\">\n        <MobileDrawerTitle>Filters</MobileDrawerTitle>\n        <div className=\"px-6 pb-6 space-y-3\">\n          {['Category', 'Price', 'Rating', 'Brand'].map((label) => (\n            <div\n              key={label}\n              className=\"flex items-center justify-between rounded-md squircle-corners border border-(--color-border) bg-(--color-surface) px-4 py-3 text-sm\"\n            >\n              <span className=\"text-(--color-fg)\">{label}</span>\n              <span className=\"text-(--color-muted)\">Any</span>\n            </div>\n          ))}\n        </div>\n      </MobileDrawer>\n    </div>\n  )\n}\n"
    },
    {
      "path": "lib/hooks/use-focus-trap.ts",
      "type": "registry:lib",
      "content": "'use client'\n\nimport { useEffect, useRef, type RefObject } from 'react'\n\nexport function useFocusTrap(\n  containerRef: RefObject<HTMLElement | null>,\n  active: boolean,\n  restoreRef?: RefObject<HTMLElement | null>,\n) {\n  const previouslyFocusedRef = useRef<HTMLElement | null>(null)\n\n  useEffect(() => {\n    if (!active) return\n\n    previouslyFocusedRef.current = document.activeElement as HTMLElement | null\n    const restoreTarget = restoreRef?.current ?? previouslyFocusedRef.current\n\n    const container = containerRef.current\n    if (!container) return\n\n    const focusable = container.querySelectorAll<HTMLElement>(\n      'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex=\"-1\"])',\n    )\n    if (focusable.length) {\n      ;(focusable[0] as HTMLElement).focus()\n    }\n\n    const handleKeyDown = (e: KeyboardEvent) => {\n      if (e.key !== 'Tab') return\n\n      const focusableEls = container.querySelectorAll<HTMLElement>(\n        'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex=\"-1\"])',\n      )\n      if (!focusableEls.length) return\n\n      const first = focusableEls[0] as HTMLElement\n      const last = focusableEls[focusableEls.length - 1] as HTMLElement\n\n      if (e.shiftKey) {\n        if (document.activeElement === first) {\n          e.preventDefault()\n          last.focus()\n        }\n      } else {\n        if (document.activeElement === last) {\n          e.preventDefault()\n          first.focus()\n        }\n      }\n    }\n\n    document.addEventListener('keydown', handleKeyDown)\n    return () => {\n      document.removeEventListener('keydown', handleKeyDown)\n      if (restoreTarget && typeof restoreTarget.focus === 'function') {\n        restoreTarget.focus()\n      }\n    }\n  }, [active, containerRef, restoreRef])\n}\n"
    }
  ],
  "type": "registry:ui"
}
