{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "sheet",
  "title": "Sheet",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/sheet/sheet.component.ts",
      "content": "import {\n  Component,\n  Directive,\n  ElementRef,\n  EventEmitter,\n  Input,\n  OnChanges,\n  OnDestroy,\n  Output,\n  TemplateRef,\n  ViewChild,\n  ViewContainerRef,\n  booleanAttribute,\n  inject,\n  signal,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { cn } from '@/lib/utils'\nimport {\n  BodyPortal,\n  afterExitAnimation,\n  lockScroll,\n  pushDismissableLayer,\n  trapFocus,\n  uniqueId,\n} from '@/ui/popper/popper'\n\nexport type SheetSide = 'top' | 'right' | 'bottom' | 'left'\n\n/**\n * Cancelable event emitted by Content's dismiss / focus outputs. Mirrors Radix (and the\n * sibling dialog): call `event.preventDefault()` to keep the sheet open (escapeKeyDown,\n * pointerDownOutside, interactOutside) or to skip the automatic focus move (openAutoFocus,\n * closeAutoFocus). `detail.originalEvent` is the DOM event that caused it.\n */\nexport type SheetDismissEvent = CustomEvent<{ originalEvent: Event }>\n\nconst dismissEvent = (type: string, originalEvent: Event): SheetDismissEvent =>\n  new CustomEvent(type, { cancelable: true, detail: { originalEvent } })\n\n/**\n * Angular port of UIPKGE Sheet with Radix Dialog behaviour (what the React / Vue\n * sheets use): content renders in a body portal over an overlay, focus is trapped\n * and restored, page scroll locks while open, Escape and overlay clicks dismiss\n * (cancelable through the Radix-named outputs), title / description wire\n * aria-labelledby / aria-describedby. Trigger and Close\n * are directives (the `asChild` equivalent). Tailwind class strings identical to\n * the Vue / React sources.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-sheet, [ui-sheet]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"sheet\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-state]': 'isOpen ? \"open\" : \"closed\"',\n    class: 'contents',\n  },\n  template: `<ng-content />`,\n})\nexport class UiSheetComponent implements OnChanges {\n  @Input() open?: boolean\n  @Input() defaultOpen = false\n  @Input({ transform: booleanAttribute }) modal = true\n  @Output() openChange = new EventEmitter<boolean>()\n\n  readonly contentId = uniqueId('sheet-content')\n  titleId?: string\n  descriptionId?: string\n  trigger?: HTMLElement\n  content?: UiSheetContentComponent\n  private readonly _open = signal<boolean | null>(null)\n\n  get isOpen(): boolean {\n    if (this.open !== undefined) return this.open\n    return this._open() ?? this.defaultOpen\n  }\n\n  ngOnChanges(): void {\n    this.content?.sync()\n  }\n\n  setOpen(value: boolean): void {\n    if (value === this.isOpen) return\n    this._open.set(value)\n    this.openChange.emit(value)\n    this.content?.sync()\n  }\n\n  toggle(): void {\n    this.setOpen(!this.isOpen)\n  }\n}\n\nexport const SHEET_OVERLAY_CLASS =\n  'motion-safe:data-[state=open]:animate-in motion-safe:data-[state=closed]:animate-out motion-safe:data-[state=open]:ease-emphasized motion-safe:data-[state=open]:blur-in-2 motion-safe:data-[state=closed]:blur-out-2 motion-safe:data-[state=closed]:fade-out-0 motion-safe:data-[state=open]:fade-in-0 bg-foreground/50 fixed inset-0 z-50'\n\n/** Standalone overlay part (the content renders its own overlay, as in React). */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-sheet-overlay, [ui-sheet-overlay]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"sheet-overlay\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-state]': 'sheet ? (sheet.isOpen ? \"open\" : \"closed\") : null',\n    '[class]': 'hostClass',\n  },\n  template: `<ng-content />`,\n})\nexport class UiSheetOverlayComponent {\n  readonly sheet = inject(UiSheetComponent, { optional: true })\n  @Input('class') className?: string\n\n  get hostClass(): string {\n    return cn(SHEET_OVERLAY_CLASS, this.className)\n  }\n}\n\nexport function sheetContentClass(side: SheetSide, className?: string): string {\n  return cn(\n    'bg-background motion-safe:data-[state=open]:animate-in motion-safe:data-[state=closed]:animate-out motion-safe:data-[state=open]:ease-emphasized motion-safe:data-[state=open]:blur-in-2 motion-safe:data-[state=closed]:blur-out-2 fixed z-50 flex flex-col gap-4 overflow-hidden shadow-lg transition ease-in-out motion-safe:data-[state=closed]:duration-200 motion-safe:data-[state=open]:duration-300',\n    side === 'right' &&\n      'motion-safe:data-[state=closed]:slide-out-to-right motion-safe:data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm',\n    side === 'left' &&\n      'motion-safe:data-[state=closed]:slide-out-to-left motion-safe:data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm',\n    side === 'top' &&\n      'motion-safe:data-[state=closed]:slide-out-to-top motion-safe:data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b',\n    side === 'bottom' &&\n      'motion-safe:data-[state=closed]:slide-out-to-bottom motion-safe:data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t',\n    className,\n  )\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-sheet-content, [ui-sheet-content]',\n  standalone: true,\n  host: { class: 'hidden' },\n  template: `\n    <ng-template #panel>\n      @if (sheet.modal) {\n        <div data-slot=\"sheet-overlay\" data-uipkge=\"\" [attr.data-state]=\"state()\" [class]=\"overlayClass\"></div>\n      }\n      <div\n        #panelEl\n        [id]=\"sheet.contentId\"\n        role=\"dialog\"\n        [attr.aria-modal]=\"sheet.modal ? 'true' : null\"\n        tabindex=\"-1\"\n        data-slot=\"sheet-content\"\n        data-uipkge=\"\"\n        [attr.data-state]=\"state()\"\n        [attr.data-side]=\"side\"\n        [attr.aria-labelledby]=\"sheet.titleId || null\"\n        [attr.aria-describedby]=\"sheet.descriptionId || null\"\n        [class]=\"panelClass\"\n      >\n        <ng-content />\n        <button\n          type=\"button\"\n          data-slot=\"sheet-close\"\n          class=\"ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none\"\n          (click)=\"sheet.setOpen(false)\"\n        >\n          <svg\n            xmlns=\"http://www.w3.org/2000/svg\"\n            width=\"24\"\n            height=\"24\"\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            stroke-width=\"2\"\n            stroke-linecap=\"round\"\n            stroke-linejoin=\"round\"\n            class=\"lucide lucide-x size-4\"\n            aria-hidden=\"true\"\n          >\n            <path d=\"M18 6 6 18\" />\n            <path d=\"m6 6 12 12\" />\n          </svg>\n          <span class=\"sr-only\">Close</span>\n        </button>\n      </div>\n    </ng-template>\n  `,\n})\nexport class UiSheetContentComponent implements OnChanges, OnDestroy {\n  readonly sheet = inject(UiSheetComponent)\n  private readonly portal = new BodyPortal(inject(ViewContainerRef))\n  @Input('class') className?: string\n  @Input() side: SheetSide = 'right'\n  /** Extra attributes for the dialog element (React spreads props onto SheetContent). */\n  @Input() attributes: Record<string, string> = {}\n  @Output() closed = new EventEmitter<void>()\n  @Output() openAutoFocus = new EventEmitter<SheetDismissEvent>()\n  @Output() closeAutoFocus = new EventEmitter<SheetDismissEvent>()\n  @Output() escapeKeyDown = new EventEmitter<SheetDismissEvent>()\n  @Output() pointerDownOutside = new EventEmitter<SheetDismissEvent>()\n  @Output() interactOutside = new EventEmitter<SheetDismissEvent>()\n  @ViewChild('panel', { static: true }) panelTpl!: TemplateRef<unknown>\n  readonly overlayClass = SHEET_OVERLAY_CLASS\n  readonly state = signal<'open' | 'closed'>('closed')\n  private panelEl?: HTMLElement\n  private restoreFocusTo: HTMLElement | null = null\n  private interactedOutside = false\n  private cleanups: (() => void)[] = []\n\n  constructor() {\n    this.sheet.content = this\n  }\n\n  get panelClass(): string {\n    return sheetContentClass(this.side, this.className)\n  }\n\n  ngOnChanges(): void {\n    if (this.portal.attached) this.sync()\n  }\n\n  sync(): void {\n    if (this.sheet.isOpen && (!this.portal.attached || this.state() === 'closed')) {\n      if (this.portal.attached) this.portal.detach()\n      this.show()\n    } else if (!this.sheet.isOpen && this.portal.attached && this.state() === 'open') void this.hide()\n  }\n\n  private show(): void {\n    this.restoreFocusTo = document.activeElement as HTMLElement | null\n    this.interactedOutside = false\n    this.state.set('open')\n    const host = this.portal.attach(this.panelTpl)\n    const panel = host.querySelector<HTMLElement>('[data-slot=\"sheet-content\"]')!\n    for (const [name, value] of Object.entries(this.attributes)) panel.setAttribute(name, value)\n    this.panelEl = panel\n    this.cleanups.push(\n      pushDismissableLayer({\n        contains: (t) => panel.contains(t),\n        onEscape: (e) => {\n          const event = dismissEvent('escapeKeyDown', e)\n          this.escapeKeyDown.emit(event)\n          if (!event.defaultPrevented) this.close()\n        },\n        onPointerDownOutside: (e) => {\n          // Non-modal: the trigger toggles on its own click; don't dismiss first.\n          if (!this.sheet.modal && this.sheet.trigger?.contains(e.target as Node)) return\n          const event = dismissEvent('pointerDownOutside', e)\n          this.pointerDownOutside.emit(event)\n          this.interactOutside.emit(event)\n          if (event.defaultPrevented) return\n          this.interactedOutside = true\n          this.close()\n        },\n      }),\n    )\n    // Radix: only a modal dialog traps focus, locks scroll and renders the overlay.\n    if (this.sheet.modal) this.cleanups.push(trapFocus(panel), lockScroll())\n    queueMicrotask(() => {\n      if (!panel.isConnected) return\n      const event = dismissEvent('openAutoFocus', new Event('focus'))\n      this.openAutoFocus.emit(event)\n      if (event.defaultPrevented) return\n      const first = panel.querySelector<HTMLElement>(\n        '[autofocus],a[href],button:not([disabled]),input:not([disabled]),select,textarea,[tabindex]:not([tabindex=\"-1\"])',\n      )\n      ;(first ?? panel).focus({ preventScroll: true })\n      // Radix FocusScope focuses the first tabbable with { select: true }: text inputs get selected.\n      if (first instanceof HTMLInputElement) first.select()\n    })\n  }\n\n  private close(): void {\n    this.sheet.setOpen(false)\n    this.closed.emit()\n  }\n\n  private async hide(): Promise<void> {\n    this.state.set('closed')\n    this.cleanups.splice(0).forEach((fn) => fn())\n    await afterExitAnimation(this.panelEl, 300)\n    if (this.sheet.isOpen) return\n    this.portal.detach()\n    this.panelEl = undefined\n    const event = dismissEvent('closeAutoFocus', new Event('blur'))\n    this.closeAutoFocus.emit(event)\n    if (event.defaultPrevented || (!this.sheet.modal && this.interactedOutside)) return\n    this.restoreFocusTo?.focus?.({ preventScroll: true })\n  }\n\n  ngOnDestroy(): void {\n    this.cleanups.splice(0).forEach((fn) => fn())\n    this.portal.detach()\n  }\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-sheet-header, [ui-sheet-header]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"sheet-header\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[class]': 'hostClass',\n  },\n  template: `<ng-content />`,\n})\nexport class UiSheetHeaderComponent {\n  @Input('class') className?: string\n\n  get hostClass(): string {\n    return cn('flex flex-col gap-1.5 p-4', this.className)\n  }\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-sheet-footer, [ui-sheet-footer]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"sheet-footer\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[class]': 'hostClass',\n  },\n  template: `<ng-content />`,\n})\nexport class UiSheetFooterComponent {\n  @Input('class') className?: string\n\n  get hostClass(): string {\n    return cn('mt-auto flex flex-col gap-2 p-4', this.className)\n  }\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-sheet-title, [ui-sheet-title]',\n  standalone: true,\n  host: {\n    '[attr.id]': 'id',\n    '[attr.data-slot]': '\"sheet-title\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[class]': 'hostClass',\n  },\n  template: `<ng-content />`,\n})\nexport class UiSheetTitleComponent {\n  private readonly sheet = inject(UiSheetComponent, { optional: true })\n  readonly id = uniqueId('sheet-title')\n  @Input('class') className?: string\n\n  constructor() {\n    if (this.sheet) this.sheet.titleId = this.id\n  }\n\n  get hostClass(): string {\n    return cn('block text-foreground font-semibold', this.className)\n  }\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-sheet-description, [ui-sheet-description]',\n  standalone: true,\n  host: {\n    '[attr.id]': 'id',\n    '[attr.data-slot]': '\"sheet-description\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[class]': 'hostClass',\n  },\n  template: `<ng-content />`,\n})\nexport class UiSheetDescriptionComponent {\n  private readonly sheet = inject(UiSheetComponent, { optional: true })\n  readonly id = uniqueId('sheet-description')\n  @Input('class') className?: string\n\n  constructor() {\n    if (this.sheet) this.sheet.descriptionId = this.id\n  }\n\n  get hostClass(): string {\n    return cn('block text-muted-foreground text-sm', this.className)\n  }\n}\n\n@Directive({\n  selector: 'ui-sheet-trigger, [ui-sheet-trigger]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': 'dataSlot',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.aria-haspopup]': '\"dialog\"',\n    '[attr.aria-expanded]': 'sheet.isOpen',\n    '[attr.aria-controls]': 'sheet.isOpen ? sheet.contentId : null',\n    '[attr.data-state]': 'sheet.isOpen ? \"open\" : \"closed\"',\n    '(click)': 'sheet.toggle()',\n  },\n})\nexport class UiSheetTriggerComponent {\n  // Radix Slot: the trigger's data-slot overrides a wrapped component's own (ui-button's\n  // \"button\"), but a data-slot written on the element itself wins.\n  readonly dataSlot: string = inject(ElementRef).nativeElement.getAttribute('data-slot') ?? 'sheet-trigger'\n  readonly sheet = inject(UiSheetComponent)\n\n  constructor() {\n    this.sheet.trigger = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement\n  }\n}\n\n@Directive({\n  selector: 'ui-sheet-close, [ui-sheet-close]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"sheet-close\"',\n    '[attr.data-uipkge]': '\"\"',\n    '(click)': 'sheet.setOpen(false)',\n  },\n})\nexport class UiSheetCloseComponent {\n  readonly sheet = inject(UiSheetComponent)\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/sheet/sheet.component.ts"
    },
    {
      "path": "packages/registry-angular/components/sheet/index.ts",
      "content": "export {\n  UiSheetComponent,\n  UiSheetOverlayComponent,\n  UiSheetContentComponent,\n  UiSheetHeaderComponent,\n  UiSheetFooterComponent,\n  UiSheetTitleComponent,\n  UiSheetDescriptionComponent,\n  UiSheetTriggerComponent,\n  UiSheetCloseComponent,\n  SHEET_OVERLAY_CLASS,\n  sheetContentClass,\n  type SheetSide,\n  type SheetDismissEvent,\n} from './sheet.component'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/sheet/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/popper.json"
  ],
  "description": "Side-mounted modal that slides in from the top, right, bottom, or left edge. Use for filter panels, edit drawers, and mobile menus. Focus trap, scroll lock and Escape / outside-click dismissal come from the built-in popper helpers — no @angular/cdk needed.",
  "categories": [
    "overlay"
  ]
}