{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "alert-modal",
  "title": "Alert Modal",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/alert-modal/alert-modal.component.ts",
      "content": "import {\n  Component,\n  Directive,\n  ElementRef,\n  EventEmitter,\n  Input,\n  OnDestroy,\n  Output,\n  TemplateRef,\n  ViewChild,\n  booleanAttribute,\n  inject,\n  signal,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { cn } from '@/lib/utils'\nimport { buttonVariants } from '@/ui/button/button.component'\nimport {\n  UiDialogComponent,\n  UiDialogContentComponent,\n  UiDialogDescriptionComponent,\n  UiDialogTitleComponent,\n  type DialogDismissEvent,\n} from '@/ui/dialog/dialog.component'\nimport { UiRenderTemplateDirective } from '@/ui/popper/popper'\n\nexport type AlertModalTone = 'default' | 'destructive' | 'success' | 'warning'\nexport type AlertModalIcon = 'info' | 'warning' | 'error' | 'success'\n\nconst TONE_ICON: Record<AlertModalTone, string> = {\n  destructive: 'text-destructive',\n  success: 'text-success',\n  warning: 'text-warning',\n  default: 'text-muted-foreground',\n}\n\nconst TONE_ACTION: Record<AlertModalTone, string> = {\n  destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',\n  success: 'bg-success text-success-foreground hover:bg-success/90',\n  warning: 'bg-warning text-warning-foreground hover:bg-warning/90',\n  default: '',\n}\n\nconst isBuiltInIcon = (value: unknown): value is AlertModalIcon =>\n  value === 'info' || value === 'success' || value === 'warning' || value === 'error'\n\n/**\n * Angular port of UIPKGE AlertModal, composed from Dialog exactly like the React\n * component: a role=\"alertdialog\" DialogContent (portal, overlay, focus trap + restore,\n * scroll lock, Escape) that does NOT soft-dismiss on outside clicks (WAI-ARIA), a\n * tone-colored icon ring, cancel (closes) + action (emits `action`, the consumer\n * closes) buttons, and a `loading` state that keeps the modal open and disables both\n * buttons. Open it with a `[ui-alert-modal-trigger]` element projected inside, or drive\n * `open` / `openChange` yourself.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-alert-modal, [ui-alert-modal]',\n  standalone: true,\n  imports: [\n    UiDialogComponent,\n    UiDialogContentComponent,\n    UiDialogTitleComponent,\n    UiDialogDescriptionComponent,\n    UiRenderTemplateDirective,\n  ],\n  // React renders no wrapper element. A static title=\"...\" input would also land on the\n  // host as a native tooltip over the trigger: strip it.\n  host: { class: 'contents', '[attr.title]': 'null' },\n  template: `\n    <ui-dialog [open]=\"isOpen\" (openChange)=\"setOpen($event)\">\n      <ng-content select=\"[ui-alert-modal-trigger], [slot=trigger]\" />\n      <ui-dialog-content\n        role=\"alertdialog\"\n        [showCloseButton]=\"false\"\n        [class]=\"className ?? ''\"\n        (pointerDownOutside)=\"preventOutside($event)\"\n        (interactOutside)=\"preventOutside($event)\"\n      >\n        <div class=\"flex flex-col gap-2 text-center sm:text-left\">\n          @if (iconTemplate || builtInIcon) {\n            <div [class]=\"iconClass\">\n              @if (iconTemplate) {\n                <ng-container [uiRenderTemplate]=\"iconTemplate\" />\n              } @else {\n                @switch (builtInIcon) {\n                  @case ('info') {\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-info size-5\"\n                      aria-hidden=\"true\"\n                    >\n                      <circle cx=\"12\" cy=\"12\" r=\"10\" />\n                      <path d=\"M12 16v-4\" />\n                      <path d=\"M12 8h.01\" />\n                    </svg>\n                  }\n                  @case ('success') {\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-circle-check size-5\"\n                      aria-hidden=\"true\"\n                    >\n                      <circle cx=\"12\" cy=\"12\" r=\"10\" />\n                      <path d=\"m9 12 2 2 4-4\" />\n                    </svg>\n                  }\n                  @case ('warning') {\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-triangle-alert size-5\"\n                      aria-hidden=\"true\"\n                    >\n                      <path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\" />\n                      <path d=\"M12 9v4\" />\n                      <path d=\"M12 17h.01\" />\n                    </svg>\n                  }\n                  @case ('error') {\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-circle-alert size-5\"\n                      aria-hidden=\"true\"\n                    >\n                      <circle cx=\"12\" cy=\"12\" r=\"10\" />\n                      <line x1=\"12\" x2=\"12\" y1=\"8\" y2=\"12\" />\n                      <line x1=\"12\" x2=\"12.01\" y1=\"16\" y2=\"16\" />\n                    </svg>\n                  }\n                }\n              }\n            </div>\n          }\n          <h2 ui-dialog-title class=\"text-lg font-semibold\">{{ title }}</h2>\n          @if (description) {\n            <p ui-dialog-description class=\"text-muted-foreground text-sm\">{{ description }}</p>\n          }\n        </div>\n\n        <div class=\"text-sm empty:hidden\"><ng-content /></div>\n\n        <div class=\"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\">\n          @if (actions) {\n            <ng-container [uiRenderTemplate]=\"actions\" />\n          } @else {\n            @if (cancelLabel != null) {\n              <button\n                type=\"button\"\n                data-uipkge=\"\"\n                data-slot=\"dialog-close\"\n                data-variant=\"outline\"\n                data-size=\"default\"\n                [class]=\"cancelClass\"\n                [disabled]=\"loading\"\n                (click)=\"handleCancel($event)\"\n              >\n                {{ cancelLabel }}\n              </button>\n            }\n            <button\n              type=\"button\"\n              data-uipkge=\"\"\n              data-slot=\"button\"\n              data-variant=\"default\"\n              data-size=\"default\"\n              [class]=\"actionClass\"\n              [disabled]=\"loading || actionDisabled\"\n              [attr.aria-busy]=\"loading\"\n              (click)=\"handleAction($event)\"\n            >\n              @if (loading) {\n                <span\n                  class=\"mr-2 inline-block size-3.5 animate-spin rounded-full border-2 border-current border-t-transparent\"\n                  aria-hidden=\"true\"\n                ></span>\n              }\n              {{ actionLabel }}\n            </button>\n          }\n        </div>\n      </ui-dialog-content>\n    </ui-dialog>\n  `,\n})\nexport class UiAlertModalComponent {\n  /** Controlled open state (pair with `openChange`). Leave unset for uncontrolled use. */\n  @Input() open?: boolean\n  @Input() title = ''\n  @Input() description = ''\n  @Input() actionLabel = 'Continue'\n  /** Label for the cancel button. Pass `null` to hide it. */\n  @Input() cancelLabel: string | null = 'Cancel'\n  @Input() tone: AlertModalTone = 'default'\n  /** Built-in icon shortcut, or an `<ng-template>` rendering a custom icon. */\n  @Input() icon: AlertModalIcon | TemplateRef<unknown> | null = null\n  @Input({ transform: booleanAttribute }) loading = false\n  @Input({ transform: booleanAttribute }) actionDisabled = false\n  /** Classes merged onto the dialog content (React passes className to DialogContent). */\n  @Input('class') className?: string\n  /** Replaces the default cancel / action button row. */\n  @Input() actions: TemplateRef<unknown> | null = null\n\n  @Output() openChange = new EventEmitter<boolean>()\n  @Output() action = new EventEmitter<MouseEvent>()\n  @Output() cancel = new EventEmitter<MouseEvent>()\n\n  @ViewChild(UiDialogComponent, { static: true }) dialog!: UiDialogComponent\n  private readonly internalOpen = signal(false)\n\n  get isOpen(): boolean {\n    return this.open !== undefined ? this.open : this.internalOpen()\n  }\n\n  get builtInIcon(): AlertModalIcon | null {\n    return isBuiltInIcon(this.icon) ? this.icon : null\n  }\n\n  get iconTemplate(): TemplateRef<unknown> | null {\n    return this.icon instanceof TemplateRef ? this.icon : null\n  }\n\n  get iconClass(): string {\n    return cn('bg-muted mb-2 flex size-10 items-center justify-center rounded-full', TONE_ICON[this.tone])\n  }\n\n  get cancelClass(): string {\n    return buttonVariants({ variant: 'outline' })\n  }\n\n  get actionClass(): string {\n    return cn(buttonVariants({ variant: 'default' }), TONE_ACTION[this.tone])\n  }\n\n  setOpen(value: boolean): void {\n    // Keep the dialog open while an async action is in flight.\n    if (!value && this.loading) return\n    if (value === this.isOpen) return\n    if (this.open === undefined) this.internalOpen.set(value)\n    this.openChange.emit(value)\n    // Controlled: the parent's [open] binding drives the dialog on its next check.\n  }\n\n  /** Alert dialogs must not soft-dismiss on outside click (WAI-ARIA). */\n  preventOutside(event: DialogDismissEvent): void {\n    event.preventDefault()\n  }\n\n  handleAction(event: MouseEvent): void {\n    if (this.loading || this.actionDisabled) {\n      event.preventDefault()\n      return\n    }\n    this.action.emit(event)\n  }\n\n  handleCancel(event: MouseEvent): void {\n    this.cancel.emit(event)\n    this.setOpen(false)\n  }\n\n  registerTrigger(el: HTMLElement): void {\n    this.dialog.trigger = el\n  }\n}\n\n/**\n * Marks the element that opens the modal (React's `trigger` prop wrapped in\n * DialogTrigger asChild): `<button ui-button ui-alert-modal-trigger>Delete</button>`\n * projected inside `<ui-alert-modal>`.\n */\n@Directive({\n  selector: '[ui-alert-modal-trigger]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': 'dataSlot',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.type]': 'isButton ? \"button\" : null',\n    '[attr.aria-haspopup]': '\"dialog\"',\n    '[attr.aria-expanded]': 'modal.isOpen',\n    '[attr.aria-controls]': 'modal.dialog?.contentId',\n    '[attr.data-state]': 'modal.isOpen ? \"open\" : \"closed\"',\n    '(click)': 'onClick()',\n  },\n})\nexport class UiAlertModalTriggerDirective implements OnDestroy {\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') ?? 'dialog-trigger'\n  readonly modal = inject(UiAlertModalComponent)\n  private readonly el = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement\n  readonly isButton = this.el.tagName === 'BUTTON'\n\n  onClick(): void {\n    this.modal.registerTrigger(this.el)\n    this.modal.setOpen(!this.modal.isOpen)\n  }\n\n  ngOnDestroy(): void {\n    if (this.modal.dialog?.trigger === this.el) this.modal.dialog.trigger = undefined\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/alert-modal/alert-modal.component.ts"
    },
    {
      "path": "packages/registry-angular/components/alert-modal/index.ts",
      "content": "export { UiAlertModalComponent, UiAlertModalTriggerDirective } from './alert-modal.component'\nexport type { AlertModalIcon, AlertModalTone } from './alert-modal.component'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/alert-modal/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/dialog.json",
    "https://uipkge.dev/r/angular/button.json",
    "https://uipkge.dev/r/angular/popper.json"
  ],
  "description": "Props-driven shortcut for confirm and destructive prompts — pass `title`, `description`, `actionLabel`, and a `tone` and you get a fully styled modal with a leading icon ring, action button, and optional async loading state. Built on Dialog (role=alertdialog, no outside-click dismiss, focus trap, Escape). Skip it and use Dialog when you need a free-form modal instead.",
  "categories": [
    "overlay"
  ]
}