{
  "name": "sheet",
  "type": "registry:ui",
  "description": "Dialog panel pinned to a screen edge, on any of the four sides",
  "files": [
    {
      "path": "ui/sheet.tsx",
      "type": "registry:ui",
      "content": "import { fv, type VariantProps } from \"@facet-ui/react-variants\";\nimport { Dialog as DialogPrimitive } from \"@lattice-ui/react-dialog\";\nimport type { LayerInteractEvent } from \"@lattice-ui/react-layer\";\nimport { getPassthroughProps, type PassthroughProps, React, toSlotProps } from \"@lattice-ui/react-runtime\";\nimport { type ClassName, cn } from \"~/lib/utils\";\n\n/**\n * `dialog` with the panel pinned to an edge instead of centred. Same primitive,\n * same `PortalProvider` requirement, same dismissal — only the geometry differs,\n * which is why this is a `side` variant rather than a second set of parts.\n *\n * **How an edge is expressed.** `dialog` centres with `mx-auto my-auto`, which\n * Vela lowers to `AnchorPoint` 0.5 plus `Position` 0.5 on that axis. There is no\n * `ml-auto` equivalent to push a panel to one side — Vela consumes the token and\n * does nothing with it — so an edge is stated directly: `origin-*` is the\n * `AnchorPoint`, and `left-0` / `right-0` / `top-0` / `bottom-0` are the\n * `Position`. `right-0` is `UDim.new(1, 0)`, not zero; the family is measured\n * from the far edge, as it is in CSS.\n *\n * This works for the reason `dialog`'s centring does: `Dialog.Content`'s host\n * spans the screen and lays nothing out, so the panel positions itself inside it.\n *\n * **No `rounded-*` and one `border`.** A sheet is flush with the edge it sits on,\n * so there is nothing to round. shadcn draws a single border on the inner side;\n * Vela lowers `border` onto a `UIStroke`, which has no per-side thickness, so it\n * is the whole outline or none. It is the whole outline.\n *\n * One recipe object rather than seven exports: every exported name costs a Luau\n * register. See docs/decisions/luau-register-limit.md.\n */\nexport const sheetVariants = {\n  // The registry's one colour-by-name, for the reason `dialog` gives:\n  // docs/decisions/overlay-scrim.md.\n  overlay: fv(\"bg-black/50\"),\n  // No padding on the panel — shadcn puts it on the parts (`p-4` on the header\n  // and the footer) so a part can run edge to edge by dropping one class. Same\n  // split as `card`'s.\n  content: fv(\"flex-col gap-4 border border-border bg-background shadow-lg\", {\n    variants: {\n      side: {\n        // Both axes on every side, as everything here declares both: the pinned\n        // axis is the sheet's own measurement, the other spans the screen.\n        //\n        // `w-96` is shadcn's `sm:max-w-sm`, the width its `w-3/4` is capped at\n        // on anything wider than a phone. Vela has no max-width — `max-w-sm`\n        // reads `sm` as a spacing key and warns — so the cap is stated as the\n        // width.\n        top: \"origin-top-left top-0 left-0 w-full h-fit\",\n        bottom: \"origin-bottom-left bottom-0 left-0 w-full h-fit\",\n        left: \"origin-top-left top-0 left-0 w-96 h-full\",\n        right: \"origin-top-right top-0 right-0 w-96 h-full\",\n      },\n    },\n    defaultVariants: { side: \"right\" },\n  }),\n  // `mt-4 mr-4` is where shadcn's `top-4 right-4` puts it. The panel carries no\n  // padding of its own now, so the inset has to come from the button.\n  close: fv(\"size-6 self-end mt-4 mr-4 rounded-md text-sm font-normal text-muted-foreground hover:bg-accent\"),\n  header: fv(\"flex-col w-full h-fit gap-1.5 p-4\"),\n  // A column, not a row. `dialog`'s footer is a row of buttons; a sheet's is a\n  // stack, and shadcn pushes it to the bottom with `mt-auto` — a token Vela\n  // rejects outright (\"use a spacing key, or mx-auto/my-auto\"), so the footer\n  // sits where the column leaves it.\n  footer: fv(\"flex-col w-full h-fit gap-2 p-4\"),\n  // No `text-lg`: a sheet's title is body-sized and bold, unlike a dialog's.\n  title: fv(\"w-full h-fit whitespace-normal text-left text-base font-semibold text-foreground\"),\n  description: fv(\"w-full h-fit whitespace-normal leading-tight text-left text-sm font-normal text-muted-foreground\"),\n};\n\n/** Unstyled, like `dialog`'s: the trigger is whatever the consumer puts in it. */\nexport const Sheet = DialogPrimitive.Root;\nexport const SheetTrigger = DialogPrimitive.Trigger;\nexport const SheetPortal = DialogPrimitive.Portal;\nexport const SheetClose = DialogPrimitive.Close;\n\nexport type SheetOverlayProps = {\n  className?: ClassName;\n} & PassthroughProps<TextButton>;\n\nexport type SheetContentProps = VariantProps<typeof sheetVariants.content> & {\n  className?: ClassName;\n  /** Styles the dim behind the panel. `SheetContent` renders its own overlay. */\n  overlayClassName?: ClassName;\n  /** The ✕ at the top of the panel. Pass `false` when a footer button is the only way out. */\n  showCloseButton?: boolean;\n  /** Fires before an outside press dismisses; `event.preventDefault()` keeps the sheet open. */\n  onPointerDownOutside?: (event: LayerInteractEvent) => void;\n  onInteractOutside?: (event: LayerInteractEvent) => void;\n  children?: React.ReactNode;\n} & PassthroughProps<Frame>;\n\nexport type SheetSectionProps = {\n  className?: ClassName;\n  children?: React.ReactNode;\n} & PassthroughProps<Frame>;\n\nexport type SheetTextProps = {\n  className?: ClassName;\n  Text?: string;\n} & PassthroughProps<TextLabel>;\n\nconst NEUTRAL_PROPS = {\n  BackgroundTransparency: 1,\n  BorderSizePixel: 0,\n};\n\nconst OVERLAY_OWN_PROPS = [\"className\", \"children\"] as const;\nconst CONTENT_OWN_PROPS = [\n  \"side\",\n  \"className\",\n  \"overlayClassName\",\n  \"showCloseButton\",\n  \"onPointerDownOutside\",\n  \"onInteractOutside\",\n  \"children\",\n] as const;\nconst SECTION_OWN_PROPS = [\"className\", \"children\"] as const;\nconst TEXT_OWN_PROPS = [\"className\", \"Text\"] as const;\n\n// The forwarded bag is widened by `toSlotProps` and then has `children` dropped\n// from its *type*: the overlay types `children` as the single element `asChild`\n// merges onto, and the bag never carries one — `children` is an own prop — so\n// only the type needs narrowing. Same crossing point as `dialog`'s.\nfunction forwardProps(props: object, ownKeys: readonly string[]): { key?: React.Key } & { [index: string]: unknown } {\n  return toSlotProps(getPassthroughProps(props, ownKeys));\n}\n\nexport function SheetOverlay(props: SheetOverlayProps) {\n  return (\n    <DialogPrimitive.Overlay\n      className={cn(sheetVariants.overlay({ className: props.className }))}\n      {...forwardProps(props, OVERLAY_OWN_PROPS)}\n    />\n  );\n}\n\n/**\n * Portal, overlay and panel in one part, like `dialog`'s.\n *\n * The panel is a frame *inside* `Dialog.Content` rather than `Dialog.Content`\n * itself: the primitive forces `Size` on its own host so the layer spans the\n * screen, and a `className` there would fight it.\n */\nexport function SheetContent(props: SheetContentProps) {\n  return (\n    <DialogPrimitive.Portal>\n      {/* The primitive rather than `SheetOverlay`, so `overlayClassName` and the\n          recipe meet in one `className` expression — see `dialog`. */}\n      <DialogPrimitive.Overlay className={cn(sheetVariants.overlay({ className: props.overlayClassName }))} />\n      <DialogPrimitive.Content\n        onInteractOutside={props.onInteractOutside}\n        onPointerDownOutside={props.onPointerDownOutside}\n      >\n        <frame\n          className={cn(sheetVariants.content({ side: props.side, className: props.className }))}\n          {...NEUTRAL_PROPS}\n          {...getPassthroughProps<Frame>(props, CONTENT_OWN_PROPS)}\n        >\n          {/* Its own line at the top rather than floating in the corner: a\n              `UIListLayout` positions every child it has, so `self-end` (a\n              `UIFlexItem`) is what pushes it to the panel's edge. The glyph is\n              text — replace it to use your own artwork. */}\n          {props.showCloseButton === false ? undefined : (\n            <DialogPrimitive.Close className={cn(sheetVariants.close())} Text=\"✕\" />\n          )}\n          {props.children}\n        </frame>\n      </DialogPrimitive.Content>\n    </DialogPrimitive.Portal>\n  );\n}\n\nexport function SheetHeader(props: SheetSectionProps) {\n  return (\n    <frame\n      className={cn(sheetVariants.header({ className: props.className }))}\n      {...NEUTRAL_PROPS}\n      {...getPassthroughProps<Frame>(props, SECTION_OWN_PROPS)}\n    >\n      {props.children}\n    </frame>\n  );\n}\n\nexport function SheetFooter(props: SheetSectionProps) {\n  return (\n    <frame\n      className={cn(sheetVariants.footer({ className: props.className }))}\n      {...NEUTRAL_PROPS}\n      {...getPassthroughProps<Frame>(props, SECTION_OWN_PROPS)}\n    >\n      {props.children}\n    </frame>\n  );\n}\n\nexport function SheetTitle(props: SheetTextProps) {\n  return (\n    <textlabel\n      className={cn(sheetVariants.title({ className: props.className }))}\n      Text={props.Text ?? \"\"}\n      {...NEUTRAL_PROPS}\n      {...getPassthroughProps<TextLabel>(props, TEXT_OWN_PROPS)}\n    />\n  );\n}\n\nexport function SheetDescription(props: SheetTextProps) {\n  return (\n    <textlabel\n      className={cn(sheetVariants.description({ className: props.className }))}\n      Text={props.Text ?? \"\"}\n      {...NEUTRAL_PROPS}\n      {...getPassthroughProps<TextLabel>(props, TEXT_OWN_PROPS)}\n    />\n  );\n}\n"
    }
  ],
  "registryDependencies": [
    "utils"
  ],
  "dependencies": [
    "@facet-ui/react-variants@^0.1.1",
    "@lattice-ui/react-runtime@^0.8.0",
    "@lattice-ui/react-dialog@^0.8.0",
    "@lattice-ui/react-layer@^0.8.0"
  ],
  "providers": [
    {
      "name": "PortalProvider",
      "package": "@lattice-ui/react-layer",
      "props": {
        "container": "player-gui"
      },
      "reason": "Lattice reads the portal target from it, and throws when a dialog opens without one"
    }
  ],
  "tokens": [
    "background",
    "border",
    "foreground",
    "muted-foreground",
    "accent"
  ]
}
