{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sheet",
  "type": "registry:ui",
  "title": "Sheet",
  "description": "A panel that slides in from an edge. On phones (under 640px) side sheets become bottom sheets you can swipe down to close, unless `mobileSide=\"same\"`.",
  "dependencies": [
    "@radix-ui/react-dialog@^1.3.1",
    "tailwind-variants@^1.0.0"
  ],
  "registryDependencies": [
    "https://junco.madebytobi.com/r/dialog.json",
    "https://junco.madebytobi.com/r/icons.json",
    "https://junco.madebytobi.com/r/junco-theme.json",
    "https://junco.madebytobi.com/r/utils.json"
  ],
  "files": [
    {
      "path": "registry/junco/ui/sheet.tsx",
      "type": "registry:ui",
      "content": "import * as React from \"react\";\nimport { tv, type VariantProps } from \"tailwind-variants\";\nimport { cn } from \"@/lib/utils\";\nimport { XIcon } from \"@/lib/junco/icons\";\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\";\nimport { DialogOverlay, useReturnFocus } from \"@/components/ui/dialog\";\n\ninterface SheetContextValue {\n  setOpen: (open: boolean) => void;\n}\nconst SheetContext = React.createContext<SheetContextValue | null>(null);\n\nexport interface SheetProps extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Root> {}\n\n/**\n * A panel that slides in from an edge. On phones (under 640px) side sheets become bottom sheets\n * you can swipe down to close, unless `mobileSide=\"same\"`.\n *\n *   <Sheet>\n *     <SheetTrigger asChild><Button>Filters</Button></SheetTrigger>\n *     <SheetContent side=\"right\">\n *       <SheetHeader><SheetTitle>Filters</SheetTitle></SheetHeader>\n *       ...\n *     </SheetContent>\n *   </Sheet>\n */\nexport function Sheet({ open: openProp, defaultOpen = false, onOpenChange, ...props }: SheetProps) {\n  const [uncontrolled, setUncontrolled] = React.useState(defaultOpen);\n  const open = openProp ?? uncontrolled;\n  const setOpen = React.useCallback(\n    (next: boolean) => {\n      if (openProp === undefined) setUncontrolled(next);\n      onOpenChange?.(next);\n    },\n    [openProp, onOpenChange],\n  );\n  const ctx = React.useMemo(() => ({ setOpen }), [setOpen]);\n  return (\n    <SheetContext.Provider value={ctx}>\n      <DialogPrimitive.Root open={open} onOpenChange={setOpen} {...props} />\n    </SheetContext.Provider>\n  );\n}\n\nexport const SheetTrigger = DialogPrimitive.Trigger;\nexport const SheetClose = DialogPrimitive.Close;\n\nexport const sheetVariants = tv({\n  slots: {\n    content: [\n      \"fixed z-overlay flex flex-col gap-4 overflow-y-auto border-border bg-popover p-6 text-popover-foreground shadow-overlay\",\n      \"focus-visible:outline-none\",\n      \"data-[state=open]:animate-slide-in data-[state=closed]:animate-slide-out\",\n    ],\n    // The bar is small; the ::before gives it a finger-sized hit area.\n    handle: [\n      \"relative mx-auto -mt-3 -mb-1 hidden h-1.5 w-10 shrink-0 cursor-grab touch-none rounded-pill bg-border active:cursor-grabbing\",\n      \"before:absolute before:-inset-x-6 before:-inset-y-3 before:content-['']\",\n    ],\n    close: [\n      \"absolute top-3 right-3 grid size-8 place-items-center rounded-control text-muted-foreground\",\n      \"transition-colors duration-fast ease-standard hover:bg-accent hover:text-foreground\",\n      \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring\",\n    ],\n  },\n  variants: {\n    side: {\n      right: { content: \"inset-y-0 right-0 h-full w-[min(24rem,calc(100vw-3rem))] border-l [--slide-from:100%_0]\" },\n      left: { content: \"inset-y-0 left-0 h-full w-[min(24rem,calc(100vw-3rem))] border-r [--slide-from:-100%_0]\" },\n      top: { content: \"inset-x-0 top-0 max-h-[85dvh] border-b [--slide-from:0_-100%]\" },\n      bottom: {\n        content:\n          \"inset-x-0 bottom-0 max-h-[85dvh] rounded-t-card border-t pb-[max(1.5rem,env(safe-area-inset-bottom))] [--slide-from:0_100%]\",\n        handle: \"block\",\n      },\n    },\n    /** What side sheets do on phones. \"bottom\" (default) turns them into swipeable bottom sheets. */\n    mobileSide: {\n      bottom: {\n        content: [\n          \"max-sm:inset-x-0 max-sm:top-auto max-sm:bottom-0 max-sm:h-auto max-sm:max-h-[85dvh] max-sm:w-full\",\n          \"max-sm:rounded-t-card max-sm:border-x-0 max-sm:border-t max-sm:border-b-0 max-sm:[--slide-from:0_100%]\",\n          \"max-sm:pb-[max(1.5rem,env(safe-area-inset-bottom))]\",\n        ],\n        handle: \"max-sm:block\",\n      },\n      same: {},\n    },\n  },\n  defaultVariants: { side: \"right\", mobileSide: \"bottom\" },\n});\n\nconst MOBILE_QUERY = \"(max-width: 639px)\";\n\nexport interface SheetContentProps\n  extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>,\n    VariantProps<typeof sheetVariants> {\n  showClose?: boolean;\n  closeLabel?: string;\n  classNames?: { overlay?: string; content?: string; handle?: string; close?: string };\n}\n\nexport const SheetContent = React.forwardRef<React.ElementRef<typeof DialogPrimitive.Content>, SheetContentProps>(\n  (\n    {\n      className,\n      classNames,\n      side = \"right\",\n      mobileSide = \"bottom\",\n      showClose = true,\n      closeLabel = \"Close\",\n      onOpenAutoFocus,\n      onCloseAutoFocus,\n      children,\n      ...props\n    },\n    ref,\n  ) => {\n    const ctx = React.useContext(SheetContext);\n    const styles = sheetVariants({ side, mobileSide });\n    const contentRef = React.useRef<HTMLDivElement | null>(null);\n    const drag = React.useRef<{ y: number; t: number; dy: number } | null>(null);\n\n    const isBottom = () =>\n      side === \"bottom\" ||\n      (mobileSide === \"bottom\" && typeof window !== \"undefined\" && !!window.matchMedia?.(MOBILE_QUERY).matches);\n\n    // Swipe down on the handle to close a bottom sheet. Keyboard users have Esc and the close button.\n    const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {\n      if (!isBottom() || !ctx) return;\n      e.currentTarget.setPointerCapture(e.pointerId);\n      drag.current = { y: e.clientY, t: performance.now(), dy: 0 };\n    };\n    const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {\n      if (!drag.current || !contentRef.current) return;\n      const dy = Math.max(0, e.clientY - drag.current.y);\n      drag.current.dy = dy;\n      contentRef.current.style.translate = `0 ${dy}px`;\n    };\n    const onPointerUp = () => {\n      const d = drag.current;\n      const el = contentRef.current;\n      drag.current = null;\n      if (!d || !el) return;\n      const velocity = d.dy / Math.max(1, performance.now() - d.t);\n      if (d.dy > Math.max(80, el.offsetHeight * 0.25) || velocity > 0.5) {\n        ctx?.setOpen(false); // the slide-out picks up from where the finger let go\n      } else if (d.dy > 0) {\n        el.style.transition = \"translate calc(var(--duration-base) * var(--motion-scale)) var(--ease-spring)\";\n        el.style.translate = \"\";\n        window.setTimeout(() => (el.style.transition = \"\"), 400);\n      }\n    };\n\n    const setRefs = (node: HTMLDivElement | null) => {\n      contentRef.current = node;\n      if (typeof ref === \"function\") ref(node);\n      else if (ref) ref.current = node;\n    };\n\n    const focus = useReturnFocus(onOpenAutoFocus, onCloseAutoFocus);\n\n    return (\n      <DialogPrimitive.Portal>\n        <DialogOverlay className={classNames?.overlay} />\n        <DialogPrimitive.Content\n          ref={setRefs}\n          data-side={side}\n          className={styles.content({ className: cn(classNames?.content, className) })}\n          {...focus}\n          {...props}\n        >\n          <div\n            aria-hidden\n            data-slot=\"sheet-handle\"\n            className={styles.handle({ className: classNames?.handle })}\n            onPointerDown={onPointerDown}\n            onPointerMove={onPointerMove}\n            onPointerUp={onPointerUp}\n            onPointerCancel={onPointerUp}\n          />\n          {children}\n          {showClose && (\n            <DialogPrimitive.Close className={styles.close({ className: classNames?.close })} aria-label={closeLabel}>\n              <XIcon className=\"size-4\" />\n            </DialogPrimitive.Close>\n          )}\n        </DialogPrimitive.Content>\n      </DialogPrimitive.Portal>\n    );\n  },\n);\nSheetContent.displayName = \"SheetContent\";\n\nexport function SheetHeader({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n  return <div className={cn(\"grid gap-1.5 pr-8\", className)} {...props} />;\n}\n\nexport function SheetFooter({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n  return <div className={cn(\"mt-auto flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\", className)} {...props} />;\n}\n\nexport interface SheetTitleProps extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title> {\n  isHidden?: boolean;\n}\n\nexport const SheetTitle = React.forwardRef<React.ElementRef<typeof DialogPrimitive.Title>, SheetTitleProps>(\n  ({ className, isHidden, ...props }, ref) => (\n    <DialogPrimitive.Title\n      ref={ref}\n      className={cn(\"text-lg leading-6 font-semibold tracking-tight\", isHidden && \"sr-only\", className)}\n      {...props}\n    />\n  ),\n);\nSheetTitle.displayName = \"SheetTitle\";\n\nexport const SheetDescription = React.forwardRef<\n  React.ElementRef<typeof DialogPrimitive.Description>,\n  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>\n>(({ className, ...props }, ref) => (\n  <DialogPrimitive.Description ref={ref} className={cn(\"text-sm text-muted-foreground\", className)} {...props} />\n));\nSheetDescription.displayName = \"SheetDescription\";\n"
    }
  ],
  "docs": "Docs and examples: https://junco.madebytobi.com/docs/components/sheet",
  "meta": {
    "hash": "edb1c185b113aac8",
    "source": "packages/react/src/components/sheet.tsx",
    "exports": [
      "SheetProps",
      "Sheet",
      "SheetTrigger",
      "SheetClose",
      "sheetVariants",
      "SheetContentProps",
      "SheetContent",
      "SheetHeader",
      "SheetFooter",
      "SheetTitleProps",
      "SheetTitle",
      "SheetDescription"
    ],
    "props": {
      "Sheet": {
        "extends": "React.ComponentPropsWithoutRef<typeof DialogPrimitive.Root>",
        "props": []
      },
      "SheetContent": {
        "extends": "React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>, VariantProps<typeof sheetVariants>",
        "props": [
          {
            "name": "showClose",
            "optional": true,
            "type": "boolean"
          },
          {
            "name": "closeLabel",
            "optional": true,
            "type": "string"
          },
          {
            "name": "classNames",
            "optional": true,
            "type": "{ overlay?: string; content?: string; handle?: string; close?: string }"
          }
        ]
      },
      "SheetTitle": {
        "extends": "React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>",
        "props": [
          {
            "name": "isHidden",
            "optional": true,
            "type": "boolean"
          }
        ]
      }
    },
    "variants": {
      "variants": {
        "side": [
          "right",
          "left",
          "top",
          "bottom"
        ],
        "mobileSide": [
          "bottom",
          "same"
        ]
      },
      "defaultVariants": {
        "side": "right",
        "mobileSide": "bottom"
      }
    },
    "usage": "<Sheet>\n  <SheetTrigger asChild><Button>Filters</Button></SheetTrigger>\n  <SheetContent side=\"right\">\n    <SheetHeader><SheetTitle>Filters</SheetTitle></SheetHeader>\n    ...\n  </SheetContent>\n</Sheet>",
    "docsUrl": "https://junco.madebytobi.com/docs/components/sheet"
  }
}
