{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sheet",
  "type": "registry:ui",
  "title": "Sheet",
  "description": "An accessible React sheet for Next.js: a side panel that slides in from any edge of the screen, for forms, filters and navigation, built on Radix Dialog.",
  "author": "FewTone <https://fewtone.com>",
  "dependencies": [
    "class-variance-authority@^0.7.1",
    "lucide-react@^1.52.0",
    "motion@^14.0.0",
    "radix-ui@^1.7.0"
  ],
  "registryDependencies": [
    "https://ui.fewtone.com/r/tokens.json",
    "https://ui.fewtone.com/r/motion.json",
    "https://ui.fewtone.com/r/button.json"
  ],
  "files": [
    {
      "path": "registry/fewtone/ui/sheet.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { duration, ease, spring } from \"@/lib/motion\";\nimport { cn } from \"@/lib/utils\";\nimport { cva } from \"class-variance-authority\";\nimport { XIcon } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { Dialog as SheetPrimitive } from \"radix-ui\";\nimport { type ComponentProps, createContext, useCallback, useContext, useState } from \"react\";\n\n/*\n * Sheet: a panel that slides in from an edge of the screen, for forms, filters, navigation or details beside\n * the page. Built on Radix Dialog, so it's a modal: focus moves in and stays there, Escape and the backdrop\n * close it, and focus goes back to the trigger.\n * - The root keeps the open state, controlled (`open`) or not (`defaultOpen`), so the panel can slide out:\n *   the portal stays mounted (forceMount) inside AnimatePresence until the exit ends.\n * - Motion (.agents/rules/motion.md): a drawer slides. The panel comes in from its edge on spring.snappy and\n *   leaves on a timed duration.base + ease.in. No blur: it's a surface moving, not content appearing. The\n *   backdrop only fades. With reduced motion, both only fade.\n * - An 8px band in the panel's own colour lies just past the screen edge, so the spring's slight overshoot\n *   never opens a gap between the panel and the edge.\n * - While it leaves, clicks go through to the page, so the page is usable again at once. Focus then goes\n *   back to the trigger, unless the person has already focused something else there.\n */\n\ntype Side = \"top\" | \"right\" | \"bottom\" | \"left\";\n\nconst SheetContext = createContext<boolean | null>(null);\n\nfunction useSheetOpen(part: string) {\n  const open = useContext(SheetContext);\n  if (open === null) throw new Error(`${part} must be used inside Sheet`);\n  return open;\n}\n\n/** Leaving is timed rather than a spring, so it's over quickly */\nconst leave = { duration: duration.base, ease: ease.in };\n\n/** Where the panel waits off-screen, beyond its edge */\nconst offscreen = {\n  top: { y: \"-100%\" },\n  right: { x: \"100%\" },\n  bottom: { y: \"100%\" },\n  left: { x: \"-100%\" },\n} as const satisfies Record<Side, object>;\n\nconst sheetVariants = cva(\n  [\n    \"fixed z-(--z-modal) flex flex-col gap-4 overflow-y-auto bg-background p-6 text-foreground shadow-lg outline-hidden\",\n    // Leaving, it no longer catches clicks\n    \"data-[state=closed]:pointer-events-none\",\n  ],\n  {\n    variants: {\n      // Each side sits 8px past its edge with an 8px border in the panel's colour there (the overshoot band),\n      // and a hairline on the side facing the page\n      side: {\n        top: \"inset-x-0 -top-2 max-h-full border-t-8 border-b border-t-background border-b-border\",\n        right:\n          \"inset-y-0 -right-2 h-full w-3/4 border-r-8 border-l border-r-background border-l-border sm:max-w-sm\",\n        bottom: \"inset-x-0 -bottom-2 max-h-full border-t border-b-8 border-t-border border-b-background\",\n        left: \"inset-y-0 -left-2 h-full w-3/4 border-r border-l-8 border-r-border border-l-background sm:max-w-sm\",\n      },\n    },\n    defaultVariants: { side: \"right\" },\n  },\n);\n\nfunction Sheet({ open, defaultOpen, onOpenChange, ...props }: ComponentProps<typeof SheetPrimitive.Root>) {\n  const [own, setOwn] = useState(defaultOpen ?? false);\n  const current = open ?? own;\n  const setOpen = useCallback(\n    (next: boolean) => {\n      setOwn(next);\n      onOpenChange?.(next);\n    },\n    [onOpenChange],\n  );\n  return (\n    <SheetContext.Provider value={current}>\n      <SheetPrimitive.Root data-slot=\"sheet\" open={current} onOpenChange={setOpen} {...props} />\n    </SheetContext.Provider>\n  );\n}\n\nfunction SheetTrigger(props: ComponentProps<typeof SheetPrimitive.Trigger>) {\n  return <SheetPrimitive.Trigger data-slot=\"sheet-trigger\" {...props} />;\n}\n\nfunction SheetClose(props: ComponentProps<typeof SheetPrimitive.Close>) {\n  return <SheetPrimitive.Close data-slot=\"sheet-close\" {...props} />;\n}\n\n/** Stays mounted while the sheet is open and while its exit plays */\nfunction SheetPortal({\n  children,\n  ...props\n}: Omit<ComponentProps<typeof SheetPrimitive.Portal>, \"forceMount\">) {\n  const open = useSheetOpen(\"SheetPortal\");\n  return (\n    // initial={false}: a sheet that starts open is simply there, with no entrance\n    <AnimatePresence initial={false}>\n      {open ? (\n        <SheetPrimitive.Portal data-slot=\"sheet-portal\" {...props} forceMount>\n          {children}\n        </SheetPrimitive.Portal>\n      ) : null}\n    </AnimatePresence>\n  );\n}\n\nfunction SheetOverlay({ className, style, ...props }: ComponentProps<typeof SheetPrimitive.Overlay>) {\n  const open = useSheetOpen(\"SheetOverlay\");\n  return (\n    <SheetPrimitive.Overlay\n      data-slot=\"sheet-overlay\"\n      className={cn(\"fixed inset-0 z-(--z-overlay) bg-overlay\", className)}\n      // Radix keeps the backdrop clickable; while it fades out, clicks go through to the page\n      style={{ pointerEvents: open ? \"auto\" : \"none\", ...style }}\n      {...props}\n      asChild\n      forceMount\n    >\n      {/* The backdrop only fades: a full-screen blur would cost too much (ADR 0011) */}\n      <motion.div\n        initial={{ opacity: 0 }}\n        animate={{ opacity: 1, transition: { duration: duration.base, ease: ease.out } }}\n        exit={{ opacity: 0, transition: leave }}\n      />\n    </SheetPrimitive.Overlay>\n  );\n}\n\nexport type SheetContentProps = ComponentProps<typeof SheetPrimitive.Content> & {\n  /** The edge it slides in from */\n  side?: Side;\n  /** Show a close button in the top corner */\n  showCloseButton?: boolean;\n};\n\nfunction SheetContent({\n  className,\n  children,\n  side = \"right\",\n  showCloseButton = true,\n  onCloseAutoFocus,\n  ...props\n}: SheetContentProps) {\n  const reduce = useReducedMotion() ?? false;\n  const hidden = reduce ? { opacity: 0 } : offscreen[side];\n  return (\n    <SheetPortal>\n      <SheetOverlay />\n      <SheetPrimitive.Content\n        data-slot=\"sheet-content\"\n        data-side={side}\n        className={cn(\n          sheetVariants({ side }),\n          // The header stops short of the close button, so a long title never runs under it\n          showCloseButton && \"**:data-[slot=sheet-header]:pe-8\",\n          className,\n        )}\n        onCloseAutoFocus={(event) => {\n          onCloseAutoFocus?.(event);\n          // Once the panel is gone, focus goes back to the trigger, unless the person has already focused\n          // something on the page (clicks reach it while the panel leaves)\n          if (document.activeElement !== document.body) event.preventDefault();\n        }}\n        {...props}\n        asChild\n        forceMount\n      >\n        <motion.div\n          initial={hidden}\n          animate={\n            reduce\n              ? { opacity: 1, transition: { duration: duration.base, ease: ease.out } }\n              : { x: 0, y: 0, transition: spring.snappy }\n          }\n          exit={{ ...hidden, transition: leave }}\n        >\n          {children}\n          {showCloseButton ? (\n            <SheetPrimitive.Close asChild>\n              {/* Centred on the title's line, its icon lined up with the panel's padding */}\n              <Button\n                data-slot=\"sheet-close\"\n                variant=\"ghost\"\n                size=\"icon-sm\"\n                aria-label=\"Close\"\n                className=\"absolute end-4 top-5 text-muted-foreground hover:text-foreground\"\n              >\n                <XIcon />\n              </Button>\n            </SheetPrimitive.Close>\n          ) : null}\n        </motion.div>\n      </SheetPrimitive.Content>\n    </SheetPortal>\n  );\n}\n\nfunction SheetHeader({ className, ...props }: ComponentProps<\"div\">) {\n  return <div data-slot=\"sheet-header\" className={cn(\"grid gap-1.5\", className)} {...props} />;\n}\n\n/** Actions at the panel's foot: stacked on phones with the main one on top, in a row at the end from sm */\nfunction SheetFooter({ className, ...props }: ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"sheet-footer\"\n      className={cn(\"mt-auto flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SheetTitle({ className, ...props }: ComponentProps<typeof SheetPrimitive.Title>) {\n  return (\n    <SheetPrimitive.Title\n      data-slot=\"sheet-title\"\n      className={cn(\"text-base font-semibold\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction SheetDescription({ className, ...props }: ComponentProps<typeof SheetPrimitive.Description>) {\n  return (\n    <SheetPrimitive.Description\n      data-slot=\"sheet-description\"\n      className={cn(\"text-sm text-muted-foreground\", className)}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Sheet,\n  SheetClose,\n  SheetContent,\n  SheetDescription,\n  SheetFooter,\n  SheetHeader,\n  SheetTitle,\n  SheetTrigger,\n  sheetVariants,\n};\n"
    }
  ],
  "categories": [
    "overlays"
  ],
  "docs": "Usage and examples: https://ui.fewtone.com/components/sheet"
}
