{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"alert-dialog","type":"registry:ui","title":"Alert Dialog","description":"A question that has to be answered before anything else: Cancel, or confirm what can't be undone.","dependencies":["radix-ui"],"registryDependencies":["https://dashral.com/r/utils.json","https://dashral.com/r/lib-locale.json","https://dashral.com/r/lib-portal.json","https://dashral.com/r/button.json"],"files":[{"path":"registry/dashral/alert-dialog.tsx","type":"registry:file","target":"components/dashral/alert-dialog.tsx","content":"\"use client\"\n\nimport * as React from \"react\"\nimport { AlertDialog as AlertDialogPrimitive } from \"radix-ui\"\n\nimport { useLocale } from \"./lib/locale\"\nimport { cn } from \"./lib/utils\"\nimport { usePortalContainer } from \"./lib/portal\"\nimport { Button } from \"./button\"\n\ntype AlertDialogColor = \"primary\" | \"destructive\"\n\n// The icon's tinted circle. Every class is written out in full so Tailwind finds it.\nconst iconColors: Record<AlertDialogColor, string> = {\n  primary: \"bg-primary/10 text-primary-text\",\n  destructive: \"bg-destructive/10 text-destructive-text\",\n}\n\ntype AlertDialogProps = Omit<React.ComponentProps<typeof AlertDialogPrimitive.Root>, \"children\"> & {\n  /** What opens it, e.g. a Delete button. Leave it out to open it yourself with open and onOpenChange. */\n  trigger?: React.ReactElement\n  /** The question, e.g. \"Delete this project?\". Required: it names the dialog. */\n  title: React.ReactNode\n  /** What happens if they go ahead, e.g. \"Its 12 dashboards go with it. This can't be undone.\" */\n  description?: React.ReactNode\n  /** More between the text and the buttons, e.g. a field to type the project's name. */\n  children?: React.ReactNode\n  /** In a tinted circle beside the text, e.g. a warning triangle. */\n  icon?: React.ReactNode\n  /** \"destructive\" makes the confirm button (and the icon) red, for deleting. */\n  color?: AlertDialogColor\n  /** The confirm button's label. \"Continue\" by default. */\n  confirmLabel?: React.ReactNode\n  /** The cancel button's label. \"Cancel\" by default. */\n  cancelLabel?: React.ReactNode\n  /** Beside the spinner while onConfirm's promise runs, e.g. \"Deleting…\". */\n  loadingText?: React.ReactNode\n  /**\n   * Runs on confirm. Return a promise and the confirm button shows it's working while the dialog waits; it closes when\n   * the promise resolves and stays open if it rejects (say why in the body).\n   */\n  onConfirm?: () => void | Promise<unknown>\n  /** Keeps confirm off until something's done, e.g. the project's name typed. */\n  confirmDisabled?: boolean\n  /** The panel's classes. */\n  className?: string\n  classNames?: { title?: string; description?: string; footer?: string }\n}\n\n/**\n * A question that has to be answered before anything else, in one tag: \"Delete this project?\" with Cancel and a\n * confirm button. Unlike Dialog it can't be dismissed by clicking outside, and Cancel has the focus, so Enter never\n * deletes by accident. onConfirm can return a promise: the button shows it's working and the dialog waits for it.\n */\nfunction AlertDialog({\n  trigger,\n  title,\n  description,\n  children,\n  icon,\n  color = \"primary\",\n  confirmLabel,\n  cancelLabel,\n  loadingText,\n  onConfirm,\n  confirmDisabled,\n  className,\n  classNames,\n  open,\n  defaultOpen,\n  onOpenChange,\n  ...props\n}: AlertDialogProps) {\n  const portalContainer = usePortalContainer()\n  const { t } = useLocale()\n  const [own, setOwn] = React.useState(defaultOpen ?? false)\n  const isOpen = open ?? own\n  const [pending, setPending] = React.useState(false)\n  const change = (next: boolean) => {\n    // While onConfirm runs, it stays: Escape and Cancel wait too.\n    if (pending) return\n    if (open === undefined) setOwn(next)\n    onOpenChange?.(next)\n  }\n\n  async function confirm(event: React.MouseEvent<HTMLButtonElement>) {\n    const result = onConfirm?.()\n    if (!(result instanceof Promise)) return\n    // A promise: keep it open (Radix would close it on this click) until it settles.\n    event.preventDefault()\n    setPending(true)\n    try {\n      await result\n      setPending(false)\n      if (open === undefined) setOwn(false)\n      onOpenChange?.(false)\n    } catch {\n      setPending(false)\n    }\n  }\n\n  return (\n    <AlertDialogPrimitive.Root data-slot=\"alert-dialog\" open={isOpen} onOpenChange={change} {...props}>\n      {trigger && <AlertDialogPrimitive.Trigger asChild>{trigger}</AlertDialogPrimitive.Trigger>}\n      <AlertDialogPrimitive.Portal container={portalContainer}>\n        <AlertDialogPrimitive.Overlay\n          data-slot=\"alert-dialog-overlay\"\n          className=\"fixed inset-0 z-50 bg-black/40 backdrop-blur-[2px] duration-(--duration-standard) data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=closed]:animate-out data-[state=closed]:fade-out-0\"\n        />\n        <AlertDialogPrimitive.Content\n          data-slot=\"alert-dialog-content\"\n          {...(description ? {} : { \"aria-describedby\": undefined })}\n          className={cn(\n            \"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-5 rounded-lg border bg-background p-6 shadow-lg sm:max-w-md data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[state=open]:blur-in-[2px] data-[state=open]:duration-(--duration-standard) data-[state=open]:ease-spring data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=closed]:duration-(--duration-fast) data-[state=closed]:ease-exit motion-reduce:blur-in-0\",\n            className\n          )}\n        >\n          <div className=\"flex flex-col items-center gap-4 text-center sm:flex-row sm:items-start sm:text-start\">\n            {icon && (\n              <span data-slot=\"alert-dialog-icon\" className={cn(\"grid size-10 shrink-0 place-items-center rounded-full [&_svg]:size-5\", iconColors[color])}>\n                {icon}\n              </span>\n            )}\n            <div className=\"grid gap-2\">\n              <AlertDialogPrimitive.Title\n                data-slot=\"alert-dialog-title\"\n                className={cn(\"font-heading text-lg leading-tight font-medium tracking-[-0.02em]\", classNames?.title)}\n              >\n                {title}\n              </AlertDialogPrimitive.Title>\n              {description && (\n                <AlertDialogPrimitive.Description data-slot=\"alert-dialog-description\" className={cn(\"text-sm text-muted-foreground\", classNames?.description)}>\n                  {description}\n                </AlertDialogPrimitive.Description>\n              )}\n            </div>\n          </div>\n          {children}\n          <div data-slot=\"alert-dialog-footer\" className={cn(\"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\", classNames?.footer)}>\n            {/* Cancel takes the focus when it opens (Radix does that for the Cancel part), so Enter never confirms by accident. */}\n            <AlertDialogPrimitive.Cancel asChild>\n              <Button variant=\"outline\" disabled={pending}>\n                {cancelLabel ?? t(\"cancel\")}\n              </Button>\n            </AlertDialogPrimitive.Cancel>\n            <AlertDialogPrimitive.Action asChild>\n              <Button color={color} disabled={confirmDisabled} loading={pending} loadingText={loadingText} onClick={confirm}>\n                {confirmLabel ?? t(\"continue\")}\n              </Button>\n            </AlertDialogPrimitive.Action>\n          </div>\n        </AlertDialogPrimitive.Content>\n      </AlertDialogPrimitive.Portal>\n    </AlertDialogPrimitive.Root>\n  )\n}\n\nexport { AlertDialog, type AlertDialogProps, type AlertDialogColor }\n"}],"cssVars":{"theme":{"color-primary-text":"var(--primary-text, var(--primary))","color-destructive-foreground":"var(--destructive-foreground)","color-destructive-text":"var(--destructive-text)","ease-standard":"var(--ease-standard)","ease-enter":"var(--ease-enter)","ease-exit":"var(--ease-exit)","ease-spring":"var(--ease-spring)","font-heading":"var(--font-geist, \"Geist\"), var(--font-inter, \"Inter\"), var(--font-arabic, \"IBM Plex Sans Arabic\"), ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif"},"light":{"destructive-foreground":"oklch(0.985 0 0)","destructive-text":"#bd2800","duration-fast":"160ms","duration-standard":"240ms","duration-considered":"480ms","ease-standard":"cubic-bezier(0.2, 0, 0, 1)","ease-enter":"cubic-bezier(0, 0, 0, 1)","ease-exit":"cubic-bezier(0.3, 0, 1, 1)","ease-spring":"linear(0, 0.071 4%, 0.234 8%, 0.433 12%, 0.626 17%, 0.791 21%, 0.917 25%, 1.004 29%, 1.055 33%, 1.079 38%, 1.083 42%, 1.075 46%, 1.06 50%, 1.043 54%, 1.027 58%, 1.015 62%, 1.005 67%, 0.998 71%, 0.995 75%, 0.993 79%, 0.993 83%, 0.994 88%, 0.995 92%, 0.997 96%, 1)"},"dark":{"destructive-foreground":"oklch(0.2 0 0)","destructive-text":"#ff8a5c"}}}