{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "alert-modal",
  "title": "Alert Modal",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-svelte/components/alert-modal/AlertModal.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  export type AlertModalTone = 'default' | 'destructive' | 'success' | 'warning'\n  export type AlertModalIcon = 'info' | 'warning' | 'error' | 'success'\n\n  export interface AlertModalProps extends HTMLAttributes<HTMLDivElement> {\n    /** Controlled open state. Pair with `bind:open`. */\n    open?: boolean\n    /** Uncontrolled initial open state. */\n    defaultOpen?: boolean\n    /** Title rendered in the header. Override with the `titleSnippet`. */\n    title?: string\n    /** Description rendered under the title. Override with the `descriptionSnippet`. */\n    description?: string\n    /** Label for the primary action button. */\n    actionLabel?: string\n    /** Label for the cancel button. Pass null to hide. */\n    cancelLabel?: string | null\n    /** Visual tone — colors the icon and action button. */\n    tone?: AlertModalTone\n    /** Quick icon shortcut. Override with the `iconSnippet`. */\n    icon?: AlertModalIcon | null\n    /** Show a spinner on the action button and disable both buttons. */\n    loading?: boolean\n    /** Disable the primary action without a spinner. */\n    actionDisabled?: boolean\n    /** Clicking the trigger snippet content opens the dialog. */\n    trigger?: Snippet\n    iconSnippet?: Snippet\n    titleSnippet?: Snippet\n    descriptionSnippet?: Snippet\n    actions?: Snippet\n    ref?: HTMLDivElement | null\n    onOpenChange?: (open: boolean) => void\n    onAction?: (event: MouseEvent) => void\n    onCancel?: (event: MouseEvent) => void\n  }\n</script>\n\n<script lang=\"ts\">\n  import { CircleAlert, CircleCheck, Info, TriangleAlert } from '@lucide/svelte'\n  import { Button } from '$lib/components/ui/button'\n  import { cn } from '$lib/utils'\n\n  let {\n    open = $bindable(),\n    defaultOpen = false,\n    title = '',\n    description = '',\n    actionLabel = 'Continue',\n    cancelLabel = 'Cancel',\n    tone = 'default',\n    icon,\n    loading = false,\n    actionDisabled = false,\n    trigger,\n    iconSnippet,\n    titleSnippet,\n    descriptionSnippet,\n    actions,\n    class: className,\n    children,\n    ref = $bindable(null),\n    onOpenChange,\n    onAction,\n    onCancel,\n    ...restProps\n  }: AlertModalProps = $props()\n\n  // Sync internal state with bind:open and emit changes back out so the\n  // component works in both controlled and uncontrolled modes.\n  // Snapshot once: later `defaultOpen` changes must not reopen a user-closed dialog.\n  const getInitialOpen = () => open ?? defaultOpen\n  let internalOpen = $state(getInitialOpen())\n\n  $effect.pre(() => {\n    if (open !== undefined) internalOpen = open\n  })\n\n  const isOpen = $derived(internalOpen)\n\n  function setOpen(v: boolean) {\n    // Keep the dialog open while an async action is in flight.\n    if (!v && loading) return\n    internalOpen = v\n    open = v\n    onOpenChange?.(v)\n  }\n\n  const ResolvedIcon = $derived.by(() => {\n    switch (icon) {\n      case 'info':\n        return Info\n      case 'success':\n        return CircleCheck\n      case 'warning':\n        return TriangleAlert\n      case 'error':\n        return CircleAlert\n      default:\n        return null\n    }\n  })\n\n  const iconColorClass = $derived.by(() => {\n    switch (tone) {\n      case 'destructive':\n        return 'text-destructive'\n      case 'success':\n        return 'text-success'\n      case 'warning':\n        return 'text-warning'\n      default:\n        return 'text-muted-foreground'\n    }\n  })\n\n  const actionToneClass = $derived.by(() => {\n    switch (tone) {\n      case 'destructive':\n        return 'bg-destructive text-destructive-foreground hover:bg-destructive/90'\n      case 'success':\n        return 'bg-success text-success-foreground hover:bg-success/90'\n      case 'warning':\n        return 'bg-warning text-warning-foreground hover:bg-warning/90'\n      default:\n        return ''\n    }\n  })\n\n  // Focus the panel on open (alertdialog role) and lock body scroll while open.\n  $effect(() => {\n    if (!isOpen || !ref) return\n    ref.focus({ preventScroll: true })\n    const prev = document.body.style.overflow\n    document.body.style.overflow = 'hidden'\n    return () => {\n      document.body.style.overflow = prev\n    }\n  })\n\n  function handleAction(e: MouseEvent) {\n    if (loading || actionDisabled) {\n      e.preventDefault()\n      return\n    }\n    onAction?.(e)\n    if (!e.defaultPrevented) setOpen(false)\n  }\n\n  function handleCancel(e: MouseEvent) {\n    onCancel?.(e)\n    if (!e.defaultPrevented) setOpen(false)\n  }\n\n  const dialogId = $props.id()\n  const titleId = `${dialogId}-title`\n  const descriptionId = `${dialogId}-description`\n</script>\n\n{#if trigger}\n  <!-- Clicking (or keyboard-activating, via bubbled click) the trigger content opens the dialog. -->\n  <span data-slot=\"alert-modal-trigger\" role=\"presentation\" onclick={() => setOpen(true)}>\n    {@render trigger()}\n  </span>\n{/if}\n\n{#if isOpen}\n  <div\n    data-uipkge=\"\"\n    data-slot=\"alert-modal-overlay\"\n    class=\"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-neutral-900/80\"\n    data-state=\"open\"\n    aria-hidden=\"true\"\n  ></div>\n  <div\n    bind:this={ref}\n    role=\"alertdialog\"\n    aria-modal=\"true\"\n    aria-labelledby={titleId}\n    aria-describedby={description || descriptionSnippet ? descriptionId : undefined}\n    tabindex=\"-1\"\n    data-uipkge=\"\"\n    data-slot=\"alert-modal\"\n    data-state=\"open\"\n    data-tone={tone}\n    class={cn(\n      'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out 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 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 outline-none sm:max-w-lg',\n      className,\n    )}\n    {...restProps}\n  >\n    <div class=\"flex flex-col gap-2 text-center sm:text-left\">\n      {#if iconSnippet || ResolvedIcon}\n        <div class={cn('bg-muted mb-2 flex size-10 items-center justify-center rounded-full', iconColorClass)}>\n          {#if iconSnippet}\n            {@render iconSnippet()}\n          {:else if ResolvedIcon}\n            <ResolvedIcon class=\"size-5\" />\n          {/if}\n        </div>\n      {/if}\n      <div id={titleId} data-slot=\"alert-modal-title\" class=\"text-lg font-semibold\">\n        {#if titleSnippet}\n          {@render titleSnippet()}\n        {:else}\n          {title}\n        {/if}\n      </div>\n      {#if description || descriptionSnippet}\n        <div id={descriptionId} data-slot=\"alert-modal-description\" class=\"text-muted-foreground text-sm\">\n          {#if descriptionSnippet}\n            {@render descriptionSnippet()}\n          {:else}\n            {description}\n          {/if}\n        </div>\n      {/if}\n    </div>\n\n    {#if children}\n      <div class=\"text-sm\">\n        {@render children()}\n      </div>\n    {/if}\n\n    <div class=\"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\">\n      {#if actions}\n        {@render actions()}\n      {:else}\n        {#if cancelLabel}\n          <Button variant=\"outline\" disabled={loading} onclick={handleCancel}>\n            {cancelLabel}\n          </Button>\n        {/if}\n        <Button disabled={loading || actionDisabled} aria-busy={loading} class={actionToneClass} onclick={handleAction}>\n          {#if loading}\n            <span\n              class=\"mr-2 inline-block size-4 animate-spin rounded-full border-2 border-current border-t-transparent\"\n              aria-hidden=\"true\"\n            ></span>\n          {/if}\n          {actionLabel}\n        </Button>\n      {/if}\n    </div>\n  </div>\n{/if}\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/alert-modal/AlertModal.svelte"
    },
    {
      "path": "packages/registry-svelte/components/alert-modal/index.ts",
      "content": "export {\n  default as AlertModal,\n  type AlertModalProps,\n  type AlertModalTone,\n  type AlertModalIcon,\n} from './AlertModal.svelte'\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/alert-modal/index.ts"
    }
  ],
  "dependencies": [
    "@lucide/svelte"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/svelte/button.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. Skip it and use Dialog when you need a free-form modal instead.",
  "categories": [
    "overlay"
  ]
}