{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "alert-modal",
  "title": "Alert Modal",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-vue/components/alert-modal/AlertModal.vue",
      "content": "<script setup lang=\"ts\">\nimport { computed, ref, watch } from 'vue'\nimport type { Component, HTMLAttributes } from 'vue'\nimport { CircleAlert, CircleCheck, Info, TriangleAlert } from 'lucide-vue-next'\nimport {\n  AlertDialogAction,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogOverlay,\n  AlertDialogPortal,\n  AlertDialogRoot,\n  AlertDialogTitle,\n  AlertDialogTrigger,\n} from 'reka-ui'\nimport { cn } from '@/lib/utils'\nimport { Button } from '@/components/ui/button'\n\ntype AlertIcon = 'info' | 'warning' | 'error' | 'success'\n\nconst props = withDefaults(\n  defineProps<{\n    /** Controlled open state. Pair with v-model:open. */\n    open?: boolean\n    /** Title rendered in the header. Override with #title slot. */\n    title?: string\n    /** Description rendered under the title. Override with #description slot. */\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?: 'default' | 'destructive' | 'success' | 'warning'\n    /** Quick icon shortcut. Override with #icon slot. */\n    icon?: AlertIcon | Component | 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    class?: HTMLAttributes['class']\n  }>(),\n  {\n    title: '',\n    description: '',\n    actionLabel: 'Continue',\n    cancelLabel: 'Cancel',\n    tone: 'default',\n    loading: false,\n    actionDisabled: false,\n  },\n)\n\nconst emit = defineEmits<{\n  'update:open': [value: boolean]\n  action: [event: MouseEvent]\n  cancel: [event: MouseEvent]\n}>()\n\n// Sync internal ref with v-model:open and emit changes back out so the\n// component works in both controlled and uncontrolled modes.\nconst internalOpen = ref(props.open ?? false)\n\nwatch(\n  () => props.open,\n  (v) => {\n    if (v !== undefined && v !== internalOpen.value) internalOpen.value = v\n  },\n)\n\nfunction setOpen(v: boolean) {\n  internalOpen.value = v\n  emit('update:open', v)\n}\n\nconst builtInIcons: Record<AlertIcon, Component> = {\n  info: Info,\n  success: CircleCheck,\n  warning: TriangleAlert,\n  error: CircleAlert,\n}\n\nconst ResolvedIcon = computed<Component | null>(() => {\n  if (!props.icon) return null\n  if (typeof props.icon === 'string') return builtInIcons[props.icon] ?? null\n  return props.icon\n})\n\nconst iconColorClass = computed(() => {\n  switch (props.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\nconst actionToneClass = computed(() => {\n  switch (props.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\nfunction handleAction(e: MouseEvent) {\n  if (props.loading || props.actionDisabled) {\n    e.preventDefault()\n    return\n  }\n  emit('action', e)\n}\n\nfunction handleCancel(e: MouseEvent) {\n  emit('cancel', e)\n}\n</script>\n\n<template>\n  <AlertDialogRoot :open=\"internalOpen\" @update:open=\"setOpen\">\n    <AlertDialogTrigger v-if=\"$slots.trigger\" as-child>\n      <slot name=\"trigger\" />\n    </AlertDialogTrigger>\n\n    <AlertDialogPortal>\n      <AlertDialogOverlay\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-black/80\"\n      />\n      <AlertDialogContent\n        :class=\"\n          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 sm:max-w-lg',\n            props.class,\n          )\n        \"\n      >\n        <div class=\"flex flex-col gap-2 text-center sm:text-left\">\n          <div\n            v-if=\"$slots.icon || ResolvedIcon\"\n            :class=\"cn('bg-muted mb-2 flex size-10 items-center justify-center rounded-full', iconColorClass)\"\n          >\n            <slot name=\"icon\">\n              <component :is=\"ResolvedIcon\" v-if=\"ResolvedIcon\" class=\"size-5\" />\n            </slot>\n          </div>\n          <AlertDialogTitle class=\"text-lg font-semibold\">\n            <slot name=\"title\">{{ title }}</slot>\n          </AlertDialogTitle>\n          <AlertDialogDescription v-if=\"description || $slots.description\" class=\"text-muted-foreground text-sm\">\n            <slot name=\"description\">{{ description }}</slot>\n          </AlertDialogDescription>\n        </div>\n\n        <div v-if=\"$slots.default\" class=\"text-sm\">\n          <slot />\n        </div>\n\n        <div class=\"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\">\n          <slot name=\"actions\">\n            <AlertDialogCancel v-if=\"cancelLabel\" as-child>\n              <Button variant=\"outline\" :disabled=\"loading\" @click=\"handleCancel\">\n                {{ cancelLabel }}\n              </Button>\n            </AlertDialogCancel>\n            <AlertDialogAction as-child>\n              <Button\n                :disabled=\"loading || actionDisabled\"\n                :aria-busy=\"loading\"\n                :class=\"actionToneClass\"\n                @click=\"handleAction\"\n              >\n                <span\n                  v-if=\"loading\"\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                />\n                {{ actionLabel }}\n              </Button>\n            </AlertDialogAction>\n          </slot>\n        </div>\n      </AlertDialogContent>\n    </AlertDialogPortal>\n  </AlertDialogRoot>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/alert-modal/AlertModal.vue"
    },
    {
      "path": "packages/registry-vue/components/alert-modal/index.ts",
      "content": "export { default as AlertModal } from './AlertModal.vue'\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/alert-modal/index.ts"
    }
  ],
  "dependencies": [
    "lucide-vue-next",
    "reka-ui"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/vue/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"
  ]
}