{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sonner",
  "title": "Sonner",
  "description": "Untitled UI-styled notification toasts via the Sonner library, plus a progress-toast helper.",
  "dependencies": [
    "@untitledui/icons@^0.0.22",
    "next-themes@^0.4.6",
    "sonner@^2.0.7"
  ],
  "registryDependencies": [
    "@nx-ui/utils"
  ],
  "files": [
    {
      "path": "components/ui/sonner.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { useTheme } from \"next-themes\"\nimport { toast, Toaster as Sonner, type ToasterProps } from \"sonner\"\nimport {\n  AlertTriangle,\n  CheckCircle,\n  InfoCircle,\n  Loading02,\n  XClose,\n  XCircle,\n} from \"@untitledui/icons\"\n\nimport { cn } from \"@/lib/utils\"\n\n/**\n * Toaster — Sonner, restyled to the Untitled UI notification-toast anatomy\n * purely through Sonner's official theming surface (`toastOptions.classNames`,\n * `icons`, CSS variables). No fork, no wrapper DOM:\n *\n *   - white card (dark: elevated popover surface), `border`, `rounded-xl`,\n *     UUI `shadow-lg`, 16px padding, ~400px wide;\n *   - leading severity icon in a soft tinted circle (the UUI featured-icon\n *     \"light\" treatment) — success/error/warning tints, info maps to brand;\n *   - `text-sm font-semibold` title, `text-sm text-muted-foreground` body;\n *   - action = brand-coloured text button, cancel = gray text button;\n *   - close X top-right, shown by default (`closeButton`) as UUI toasts\n *     always carry a dismiss affordance — pass `closeButton={false}` to opt out.\n *\n * Severity is deliberately conveyed by the tinted icon only: the card itself\n * stays neutral, per Untitled UI. Sonner's `richColors` fills are therefore\n * intentionally neutralised by the card-level overrides.\n */\nconst Toaster = ({ toastOptions, ...props }: ToasterProps) => {\n  const { theme = \"system\" } = useTheme()\n\n  return (\n    <Sonner\n      theme={theme as ToasterProps[\"theme\"]}\n      className=\"toaster group !font-sans\"\n      closeButton\n      icons={{\n        success: <CheckCircle className=\"size-5\" />,\n        info: <InfoCircle className=\"size-5\" />,\n        warning: <AlertTriangle className=\"size-5\" />,\n        error: <XCircle className=\"size-5\" />,\n        loading: <Loading02 className=\"size-5 animate-spin\" />,\n        close: <XClose className=\"size-4\" />,\n      }}\n      style={\n        {\n          \"--normal-bg\": \"var(--popover)\",\n          \"--normal-text\": \"var(--popover-foreground)\",\n          \"--normal-border\": \"var(--border)\",\n          \"--border-radius\": \"var(--radius-xl)\",\n          \"--width\": \"25rem\",\n        } as React.CSSProperties\n      }\n      toastOptions={{\n        ...toastOptions,\n        classNames: {\n          toast:\n            \"group/toast !items-start !gap-3 !rounded-xl !border-border !bg-popover !p-4 !pr-8 !text-popover-foreground !shadow-lg focus-visible:!ring-4 focus-visible:!ring-ring/25\",\n          content: \"min-h-9 min-w-0 justify-center\",\n          title: \"!text-sm !font-semibold\",\n          description: \"!text-sm !text-muted-foreground\",\n          icon: cn(\n            \"!m-0 !size-9 !shrink-0 !items-center !justify-center rounded-full bg-bg-tertiary text-fg-tertiary\",\n            \"group-data-[type=info]/toast:bg-bg-brand-secondary group-data-[type=info]/toast:text-fg-brand-primary\",\n            \"group-data-[type=success]/toast:bg-bg-success-secondary group-data-[type=success]/toast:text-fg-success-primary\",\n            \"group-data-[type=warning]/toast:bg-bg-warning-secondary group-data-[type=warning]/toast:text-fg-warning-primary\",\n            \"group-data-[type=error]/toast:bg-bg-error-secondary group-data-[type=error]/toast:text-fg-error-primary\"\n          ),\n          actionButton:\n            \"!h-auto !self-center !rounded-sm !bg-transparent !p-0 !text-sm !font-semibold !text-text-brand-secondary hover:!text-text-brand-secondary_hover focus-visible:!ring-4 focus-visible:!ring-ring/25\",\n          cancelButton:\n            \"!h-auto !self-center !rounded-sm !bg-transparent !p-0 !text-sm !font-semibold !text-muted-foreground hover:!text-foreground focus-visible:!ring-4 focus-visible:!ring-ring/25\",\n          closeButton:\n            \"!top-2 !right-2 !left-auto !size-6 !transform-none !rounded-md !border-none !bg-transparent !text-fg-quaternary transition hover:!bg-bg-primary_hover hover:!text-fg-quaternary_hover focus-visible:!ring-4 focus-visible:!ring-ring/25\",\n          ...toastOptions?.classNames,\n        },\n      }}\n      {...props}\n    />\n  )\n}\n\ntype ToastWithProgressVariant = \"info\" | \"success\" | \"warning\" | \"error\"\n\ninterface ToastWithProgressOptions {\n  description?: React.ReactNode\n  /**\n   * Total time on screen in milliseconds. Must be finite (the bar drains over\n   * this window); defaults to 5000.\n   */\n  duration?: number\n  /** Severity tint for the leading icon. Info maps to the brand tint. */\n  variant?: ToastWithProgressVariant\n  /** Brand-coloured text action; the toast dismisses itself after running. */\n  action?: { label: React.ReactNode; onClick: () => void }\n  id?: string | number\n}\n\nconst progressToastIcons: Record<ToastWithProgressVariant, React.ReactNode> = {\n  info: <InfoCircle className=\"size-5\" aria-hidden=\"true\" />,\n  success: <CheckCircle className=\"size-5\" aria-hidden=\"true\" />,\n  warning: <AlertTriangle className=\"size-5\" aria-hidden=\"true\" />,\n  error: <XCircle className=\"size-5\" aria-hidden=\"true\" />,\n}\n\nconst progressToastTints: Record<ToastWithProgressVariant, string> = {\n  info: \"bg-bg-brand-secondary text-fg-brand-primary\",\n  success: \"bg-bg-success-secondary text-fg-success-primary\",\n  warning: \"bg-bg-warning-secondary text-fg-warning-primary\",\n  error: \"bg-bg-error-secondary text-fg-error-primary\",\n}\n\n/**\n * The card body rendered by `toastWithProgress` via `toast.custom`. The\n * surrounding Sonner `<li>` already carries the UUI card shell (bg, border,\n * radius, shadow, padding) from the Toaster's `classNames.toast`, so this\n * only renders the content row plus the draining time bar.\n *\n * Pause behaviour — Sonner v2 exposes no public pause/remaining-time API, so\n * the bar keeps its own clock and mirrors the exact conditions under which\n * Sonner pauses its dismiss timer (`expanded || interacting ||\n * document.hidden`): it listens for the same events, on the same\n * `[data-sonner-toaster]` element, that drive those internal flags\n * (mouseenter/mousemove/mouseleave/dragend for hover, pointerdown/up for\n * interaction, plus `visibilitychange`). Note that the `expand` prop alone\n * does NOT pause Sonner's timers — only real hover/interaction does — and the\n * bar honours that too. Sync with the actual dismiss is approximate (both\n * clocks start within the same mount frame; drift is a few ms), which is why\n * the bar clamps at zero and lets Sonner own the dismiss.\n *\n * Reduced motion: the bar's width is driven frame-by-frame from the clock\n * with no CSS transitions, easings or pulses — it persists under\n * `prefers-reduced-motion` because it conveys time, not decoration.\n */\nfunction ProgressToast({\n  id,\n  title,\n  description,\n  duration,\n  variant,\n  action,\n}: {\n  id: string | number\n  title: React.ReactNode\n  duration: number\n  variant: ToastWithProgressVariant\n} & Pick<ToastWithProgressOptions, \"description\" | \"action\">) {\n  const barRef = React.useRef<HTMLSpanElement>(null)\n\n  React.useEffect(() => {\n    const bar = barRef.current\n    if (!bar) return\n    const toaster = bar.closest(\"[data-sonner-toaster]\")\n\n    // Mirror of Sonner's internal pause flags (see JSDoc above).\n    let expanded = false\n    let interacting = false\n    let hidden = document.hidden\n\n    let elapsed = 0\n    let last: number | null = null\n    let frame = requestAnimationFrame(function tick(now: number) {\n      if (last !== null && !(expanded || interacting || hidden)) {\n        elapsed += now - last\n      }\n      last = now\n      const remaining = Math.max(0, 1 - elapsed / duration)\n      bar.style.transform = `scaleX(${remaining})`\n      if (remaining > 0) frame = requestAnimationFrame(tick)\n    })\n\n    const on = (\n      target: EventTarget,\n      type: string,\n      handler: () => void\n    ): (() => void) => {\n      target.addEventListener(type, handler)\n      return () => target.removeEventListener(type, handler)\n    }\n    const offs = [\n      on(document, \"visibilitychange\", () => {\n        hidden = document.hidden\n        // rAF stops in hidden tabs; skip the stale frame delta on resume.\n        last = null\n      }),\n    ]\n    if (toaster) {\n      offs.push(\n        on(toaster, \"mouseenter\", () => (expanded = true)),\n        on(toaster, \"mousemove\", () => (expanded = true)),\n        on(toaster, \"mouseleave\", () => {\n          if (!interacting) expanded = false\n        }),\n        on(toaster, \"dragend\", () => (expanded = false)),\n        on(toaster, \"pointerdown\", () => (interacting = true)),\n        on(toaster, \"pointerup\", () => (interacting = false))\n      )\n    }\n\n    return () => {\n      cancelAnimationFrame(frame)\n      for (const off of offs) off()\n    }\n  }, [duration])\n\n  return (\n    <div\n      data-progress-toast=\"\"\n      className=\"relative flex w-[calc(var(--width)-3rem-2px)] max-w-full items-start gap-3 pb-3\"\n    >\n      <span\n        className={cn(\n          \"flex size-9 shrink-0 items-center justify-center rounded-full\",\n          progressToastTints[variant]\n        )}\n      >\n        {progressToastIcons[variant]}\n      </span>\n      <div className=\"flex min-h-9 min-w-0 flex-1 flex-col justify-center gap-0.5\">\n        <div className=\"text-sm font-semibold\">{title}</div>\n        {description ? (\n          <div className=\"text-sm text-muted-foreground\">{description}</div>\n        ) : null}\n      </div>\n      {action ? (\n        <button\n          type=\"button\"\n          className=\"cursor-pointer self-center rounded-sm text-sm font-semibold whitespace-nowrap text-text-brand-secondary outline-none hover:text-text-brand-secondary_hover focus-visible:ring-4 focus-visible:ring-ring/25\"\n          onClick={() => {\n            action.onClick()\n            toast.dismiss(id)\n          }}\n        >\n          {action.label}\n        </button>\n      ) : null}\n      <button\n        type=\"button\"\n        aria-label=\"Dismiss\"\n        className=\"absolute -top-2 -right-6 flex size-6 cursor-pointer items-center justify-center rounded-md text-fg-quaternary transition outline-none hover:bg-bg-primary_hover hover:text-fg-quaternary_hover focus-visible:ring-4 focus-visible:ring-ring/25\"\n        onClick={() => toast.dismiss(id)}\n      >\n        <XClose className=\"size-4\" aria-hidden=\"true\" />\n      </button>\n      <span\n        aria-hidden=\"true\"\n        data-progress-bar=\"\"\n        className=\"pointer-events-none absolute inset-x-0 bottom-0 h-1 overflow-hidden rounded-full bg-bg-quaternary\"\n      >\n        <span\n          ref={barRef}\n          className=\"block h-full w-full origin-left rounded-full bg-bg-brand-solid\"\n        />\n      </span>\n    </div>\n  )\n}\n\n/**\n * toastWithProgress — a UUI-styled toast with a slim brand-coloured time bar\n * along its bottom edge that visually drains over the toast's duration,\n * pausing while the toaster is hovered (see `ProgressToast` for the honest\n * account of how the pause is kept in step with Sonner's own timer).\n *\n * @example\n *   toastWithProgress(\"Transfer note issued\", {\n *     description: \"WTN-2317 sent to Greenfield Reclamation.\",\n *     duration: 8000,\n *     variant: \"success\",\n *     action: { label: \"View\", onClick: () => {} },\n *   })\n */\nfunction toastWithProgress(\n  title: React.ReactNode,\n  options: ToastWithProgressOptions = {}\n): string | number {\n  const { description, variant = \"info\", action, id } = options\n  const duration = Number.isFinite(options.duration)\n    ? (options.duration as number)\n    : 5000\n\n  return toast.custom(\n    (toastId) => (\n      <ProgressToast\n        id={toastId}\n        title={title}\n        description={description}\n        duration={duration}\n        variant={variant}\n        action={action}\n      />\n    ),\n    { duration, ...(id !== undefined ? { id } : {}) }\n  )\n}\n\nexport { Toaster, toastWithProgress, type ToastWithProgressOptions }\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}