{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "alert-modal",
  "title": "Alert Modal",
  "type": "registry:ui",
  "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. Skip it and use Dialog when you need a free-form modal instead.",
  "categories": [
    "overlay"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/components/alert-modal/alert-modal.ts",
      "content": "import { LitElement, css, html, isServer, nothing } from 'lit'\nimport { CircleAlert, CircleCheck, Info, TriangleAlert, type IconNode } from 'lucide'\nimport { cn } from '../../lib/utils'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\nimport { icon } from '../../lib/icon'\n\nexport type AlertTone = 'default' | 'destructive' | 'success' | 'warning'\nexport type AlertIconType = 'info' | 'warning' | 'error' | 'success'\n\nconst builtInIcons: Record<AlertIconType, IconNode> = {\n  info: Info,\n  success: CircleCheck,\n  warning: TriangleAlert,\n  error: CircleAlert,\n}\n\nconst iconColorClasses: Record<AlertTone, string> = {\n  destructive: 'text-destructive',\n  success: 'text-success',\n  warning: 'text-warning',\n  default: 'text-muted-foreground',\n}\n\nconst actionToneClasses: Record<AlertTone, 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 contentClasses =\n  'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:ease-emphasized data-[state=open]:blur-in-2 data-[state=closed]:blur-out-2 data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 hidden open:grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg text-foreground m-0 backdrop:bg-foreground/50'\n\nconst trueByDefault = { fromAttribute: (v: string | null) => v !== 'false', toAttribute: () => null }\n\n/**\n * <uip-alert-modal> — AlertModal web component.\n */\nexport class UipAlertModal extends LitElement {\n  static styles = [tailwind, css`:host { display: inline-block; }`]\n\n  static properties = {\n    open: { type: Boolean, reflect: true },\n    title: { type: String },\n    description: { type: String },\n    actionLabel: { attribute: 'action-label' },\n    cancelLabel: { attribute: 'cancel-label' },\n    tone: { type: String },\n    icon: { type: String },\n    loading: { type: Boolean },\n    actionDisabled: { type: Boolean, attribute: 'action-disabled' },\n    state: { state: true },\n    hasDefaultSlot: { state: true },\n    hasActionsSlot: { state: true },\n  }\n\n  open = false\n  title = ''\n  description = ''\n  actionLabel = 'Continue'\n  cancelLabel: string | null = 'Cancel'\n  tone: AlertTone = 'default'\n  icon?: string | null = null\n  loading = false\n  actionDisabled = false\n  private state: 'open' | 'closed' = 'closed'\n  private hasDefaultSlot = false\n  private hasActionsSlot = false\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'alert-modal')\n  }\n\n  private get trigger(): HTMLElement | undefined {\n    const slot = this.renderRoot?.querySelector<HTMLSlotElement>('slot[name=\"trigger\"]')\n    return slot?.assignedElements({ flatten: true })[0] as HTMLElement | undefined\n  }\n\n  private get dialog() {\n    return this.renderRoot?.querySelector('dialog')\n  }\n\n  show() {\n    this.setOpen(true)\n  }\n\n  close() {\n    if (this.loading) return\n    this.setOpen(false)\n  }\n\n  private setOpen(open: boolean) {\n    if (this.open === open) return\n    if (!open && this.loading) return\n    this.open = open\n    this.dispatchEvent(new CustomEvent('open-change', { detail: { open }, bubbles: true, composed: true }))\n  }\n\n  protected willUpdate(changed: Map<string, unknown>) {\n    if (changed.has('open')) {\n      this.state = this.open ? 'open' : 'closed'\n    }\n  }\n\n  protected updated(changed: Map<string, unknown>) {\n    if (isServer) return\n    const trigger = this.trigger\n    if (trigger) {\n      trigger.setAttribute('aria-haspopup', 'dialog')\n      trigger.setAttribute('aria-expanded', String(this.open))\n      trigger.setAttribute('data-state', this.state)\n    }\n\n    if (!changed.has('open')) return\n    const dialog = this.dialog\n    if (!dialog) return\n\n    if (this.open && !dialog.open) {\n      dialog.showModal()\n      dialog.scrollTop = 0\n    } else if (!this.open && dialog.open) {\n      let closed = false\n      const done = () => {\n        if (closed || this.open) return\n        closed = true\n        dialog.close()\n      }\n      setTimeout(done, 300)\n      const anims = dialog.getAnimations({ subtree: true })\n      if (anims.length) Promise.all(anims.map((a) => a.finished)).then(done, done)\n      else done()\n    }\n  }\n\n  private onCancel(e: Event) {\n    // WAI-ARIA alertdialog: do not close automatically on Escape if loading\n    if (this.loading) {\n      e.preventDefault()\n      return\n    }\n    const cancelEv = new CustomEvent('cancel', { bubbles: true, composed: true, cancelable: true })\n    if (!this.dispatchEvent(cancelEv)) {\n      e.preventDefault()\n      return\n    }\n    this.setOpen(false)\n  }\n\n  private onActionClick(e: MouseEvent) {\n    if (this.loading || this.actionDisabled) {\n      e.preventDefault()\n      return\n    }\n    const ev = new CustomEvent('action', { bubbles: true, composed: true, cancelable: true })\n    if (this.dispatchEvent(ev)) {\n      this.setOpen(false)\n    }\n  }\n\n  private onCancelClick(e: MouseEvent) {\n    const ev = new CustomEvent('cancel', { bubbles: true, composed: true, cancelable: true })\n    if (this.dispatchEvent(ev)) {\n      this.setOpen(false)\n    }\n  }\n\n  private renderIcon() {\n    if (!this.icon) return nothing\n    const iconNode = builtInIcons[this.icon as AlertIconType]\n    if (!iconNode) return nothing\n    return html`\n      <div\n        class=${cn(\n          'bg-muted mb-2 flex size-10 items-center justify-center rounded-full',\n          iconColorClasses[this.tone] ?? iconColorClasses.default,\n        )}\n      >\n        ${icon(iconNode, this.icon, 'size-5')}\n      </div>\n    `\n  }\n\n  render() {\n    return html`\n      <slot name=\"trigger\" @click=${() => this.show()} @slotchange=${() => this.requestUpdate()}></slot>\n\n      <dialog\n        role=\"alertdialog\"\n        part=\"content\"\n        data-uipkge=\"\"\n        data-slot=\"alert-modal-content\"\n        data-state=${this.state}\n        class=${contentClasses}\n        @cancel=${this.onCancel}\n      >\n        <div class=\"flex flex-col gap-2 text-center sm:text-left\">\n          ${this.renderIcon()}\n          ${this.title ? html`<h2 part=\"title\" class=\"text-lg font-semibold\">${this.title}</h2>` : nothing}\n          ${this.description\n            ? html`<p part=\"description\" class=\"text-muted-foreground text-sm\">${this.description}</p>`\n            : nothing}\n        </div>\n\n        <div class=\"text-sm\">\n          <slot\n            @slotchange=${(e: Event) =>\n              (this.hasDefaultSlot = (e.target as HTMLSlotElement).assignedNodes().length > 0)}\n          ></slot>\n        </div>\n\n        <div class=\"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\">\n          <slot\n            name=\"actions\"\n            @slotchange=${(e: Event) =>\n              (this.hasActionsSlot = (e.target as HTMLSlotElement).assignedNodes().length > 0)}\n          >\n            ${this.cancelLabel !== null && this.cancelLabel !== undefined && this.cancelLabel !== 'null'\n              ? html`\n                  <button\n                    type=\"button\"\n                    part=\"cancel\"\n                    class=\"focus-visible:ring-ring border-input bg-background hover:bg-accent hover:text-accent-foreground inline-flex h-9 items-center justify-center rounded-md border px-4 py-2 text-sm font-medium transition-colors focus-visible:ring-1 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50\"\n                    ?disabled=${this.loading}\n                    @click=${this.onCancelClick}\n                  >\n                    ${this.cancelLabel}\n                  </button>\n                `\n              : nothing}\n            <button\n              type=\"button\"\n              part=\"action\"\n              class=${cn(\n                'focus-visible:ring-ring bg-primary text-primary-foreground hover:bg-primary/90 inline-flex h-9 items-center justify-center rounded-md px-4 py-2 text-sm font-medium transition-colors focus-visible:ring-1 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50',\n                actionToneClasses[this.tone],\n              )}\n              ?disabled=${this.loading || this.actionDisabled}\n              aria-busy=${this.loading ? 'true' : nothing}\n              @click=${this.onActionClick}\n            >\n              ${this.loading\n                ? html`<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                : nothing}\n              ${this.actionLabel}\n            </button>\n          </slot>\n        </div>\n      </dialog>\n    `\n  }\n}\n\ncustomElements.get('uip-alert-modal') || customElements.define('uip-alert-modal', UipAlertModal)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-alert-modal': UipAlertModal\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/alert-modal/alert-modal.ts"
    }
  ],
  "dependencies": [
    "lit",
    "lucide"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/lit-core.json"
  ]
}
