{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "toast",
  "type": "registry:ui",
  "dependencies": [
    "@base-ui/react",
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "https://usva.build/r/overlay-core.json"
  ],
  "files": [
    {
      "path": "toast.tsx",
      "target": "components/ui/toast.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\nimport { Toast as Base } from \"@base-ui/react/toast\";\nimport type * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { useScopedTheme } from \"./use-scoped-theme\";\n\nexport type ToastType = \"success\" | \"warning\" | \"danger\" | \"info\";\n\nexport interface ToastActionOptions {\n  label: string;\n  onClick?: () => void;\n}\n\nexport interface ToastOptions {\n  title: React.ReactNode;\n  description?: React.ReactNode;\n  type?: ToastType;\n  duration?: number;\n  action?: ToastActionOptions;\n}\n\nexport const toastManager = Base.createToastManager();\n\nexport function toast(options: ToastOptions): string {\n  const { duration, action, ...rest } = options;\n  return toastManager.add({\n    ...rest,\n    timeout: duration,\n    actionProps: action\n      ? { children: action.label, onClick: action.onClick }\n      : undefined,\n  });\n}\n\nexport type NotifyOptions = Omit<ToastOptions, \"title\" | \"type\">;\n\nfunction makeNotifier(type: ToastType) {\n  return (title: React.ReactNode, options?: NotifyOptions): string =>\n    toast({ ...options, title, type });\n}\n\n/**\n * Imperative sugar over `toast()`. Call from anywhere, including outside React:\n * `notify.success(\"Saved\")`, `notify.error(\"Upload failed\", { description })`.\n * Needs a mounted `Toaster` to render into, but not around the call site.\n */\nexport const notify = {\n  success: makeNotifier(\"success\"),\n  warning: makeNotifier(\"warning\"),\n  danger: makeNotifier(\"danger\"),\n  error: makeNotifier(\"danger\"),\n  info: makeNotifier(\"info\"),\n};\n\nexport interface ToasterProps {\n  limit?: number;\n  timeout?: number;\n}\n\ninterface ToneStyle {\n  icon: React.ReactNode;\n  text: string;\n  dot: string;\n  tint: string;\n}\n\nconst toneStyles: Record<ToastType, ToneStyle> = {\n  success: {\n    icon: <CheckIcon />,\n    text: \"text-success\",\n    dot: \"bg-success\",\n    tint: \"bg-success/[0.06]\",\n  },\n  warning: {\n    icon: <AlertIcon />,\n    text: \"text-warning\",\n    dot: \"bg-warning\",\n    tint: \"bg-warning/[0.06]\",\n  },\n  danger: {\n    icon: <ErrorIcon />,\n    text: \"text-danger\",\n    dot: \"bg-danger\",\n    tint: \"bg-danger/[0.07]\",\n  },\n  info: {\n    icon: <InfoIcon />,\n    text: \"text-info\",\n    dot: \"bg-info\",\n    tint: \"bg-info/[0.06]\",\n  },\n};\n\nfunction ToastItem({ toast: item }: { toast: Base.Root.ToastObject }) {\n  const tone = item.type ? toneStyles[item.type as ToastType] : undefined;\n  const duration = typeof item.timeout === \"number\" ? item.timeout : 0;\n  return (\n    <Base.Root\n      toast={item}\n      swipeDirection={[\"down\", \"right\"]}\n      className={cn(\n        \"group/toast rim-light pointer-events-auto relative isolate flex w-full max-w-sm gap-3 overflow-hidden rounded-xl border border-border bg-overlay p-4 text-ink shadow-overlay\",\n        \"transition-enter duration-slow ease-spring motion-reduce:transition-none motion-reduce:transform-none\",\n        \"data-[starting-style]:translate-y-3 data-[starting-style]:opacity-0 data-[starting-style]:blur-[3px]\",\n        \"data-[ending-style]:translate-x-4 data-[ending-style]:opacity-0 data-[ending-style]:duration-fast data-[ending-style]:ease-soft\",\n        \"data-[swiping]:transition-none motion-reduce:data-[starting-style]:translate-none motion-reduce:data-[ending-style]:translate-none\",\n      )}\n    >\n      {tone ? (\n        <>\n          <span\n            aria-hidden=\"true\"\n            className={cn(\n              \"pointer-events-none absolute inset-0 rounded-[inherit]\",\n              tone.tint,\n            )}\n          />\n          <span\n            aria-hidden=\"true\"\n            className={cn(\"relative mt-0.5 shrink-0\", tone.text)}\n          >\n            {tone.icon}\n          </span>\n        </>\n      ) : null}\n      <div className=\"relative flex min-w-0 flex-1 flex-col gap-1\">\n        <div className=\"flex items-center gap-2\">\n          {tone ? (\n            <span\n              aria-hidden=\"true\"\n              className={cn(\"size-1.5 shrink-0 rounded-full\", tone.dot)}\n            />\n          ) : null}\n          <Base.Title className=\"text-sm font-semibold text-ink\" />\n        </div>\n        <Base.Description className=\"text-sm text-muted\" />\n        <Base.Action\n          className={cn(\n            \"mt-1 self-start rounded-md border border-border-strong px-2.5 py-1 text-xs font-medium text-ink outline-none\",\n            \"transition-control duration-fast ease-soft motion-reduce:transition-none\",\n            \"hover:bg-surface-2 focus-visible:border-transparent focus-visible:ring-focus\",\n            \"active:scale-[0.98] motion-reduce:transform-none\",\n          )}\n        />\n      </div>\n      <Base.Close\n        aria-label=\"Dismiss\"\n        className={cn(\n          \"relative -m-1 flex size-6 shrink-0 items-center justify-center rounded-md text-muted outline-none\",\n          \"before:absolute before:-inset-2.5 before:content-['']\",\n          \"transition-control duration-fast ease-soft motion-reduce:transition-none motion-reduce:transform-none\",\n          \"hover:bg-surface-2 hover:text-ink active:scale-[0.96] focus-visible:ring-focus\",\n        )}\n      >\n        <CloseIcon />\n      </Base.Close>\n      {duration > 0 ? (\n        <span\n          aria-hidden=\"true\"\n          style={\n            { \"--usva-toast-duration\": `${duration}ms` } as React.CSSProperties\n          }\n          className={cn(\n            \"animate-toast-countdown pointer-events-none absolute inset-x-0 bottom-0 h-0.5 opacity-60 group-hover/toast:[animation-play-state:paused]\",\n            tone ? tone.dot : \"bg-border-strong\",\n          )}\n        />\n      ) : null}\n    </Base.Root>\n  );\n}\n\nfunction ToastList() {\n  const { toasts } = Base.useToastManager();\n  return (\n    <>\n      {toasts.map((item) => (\n        <ToastItem key={item.id} toast={item} />\n      ))}\n    </>\n  );\n}\n\n/**\n * Mount once, anywhere in the tree. It does not wrap your app: `notify` and `toast`\n * talk to a module-scoped manager, not to React context, so the call site never has\n * to sit underneath this. Toasts fired before it mounts are dropped, not queued.\n */\nexport function Toaster({ limit, timeout }: ToasterProps) {\n  const [probe, scopedTheme] = useScopedTheme();\n\n  return (\n    <Base.Provider toastManager={toastManager} limit={limit} timeout={timeout}>\n      <span ref={probe} hidden />\n      <Base.Portal>\n        <Base.Viewport\n          data-theme={scopedTheme}\n          className=\"pointer-events-none fixed bottom-4 left-4 right-4 z-toast flex w-auto flex-col-reverse gap-2 sm:left-auto sm:w-full sm:max-w-sm\"\n        >\n          <ToastList />\n        </Base.Viewport>\n      </Base.Portal>\n    </Base.Provider>\n  );\n}\nToaster.displayName = \"Toaster\";\n\nfunction CloseIcon() {\n  return (\n    <svg\n      viewBox=\"0 0 24 24\"\n      width=\"16\"\n      height=\"16\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth=\"2\"\n      strokeLinecap=\"round\"\n      aria-hidden=\"true\"\n    >\n      <path d=\"M18 6 6 18M6 6l12 12\" />\n    </svg>\n  );\n}\n\nfunction CheckIcon() {\n  return (\n    <svg\n      viewBox=\"0 0 24 24\"\n      width=\"18\"\n      height=\"18\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth=\"2\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      aria-hidden=\"true\"\n    >\n      <circle cx=\"12\" cy=\"12\" r=\"9\" />\n      <path d=\"m8.5 12 2.5 2.5 4.5-5\" />\n    </svg>\n  );\n}\n\nfunction AlertIcon() {\n  return (\n    <svg\n      viewBox=\"0 0 24 24\"\n      width=\"18\"\n      height=\"18\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth=\"2\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      aria-hidden=\"true\"\n    >\n      <path d=\"M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z\" />\n      <path d=\"M12 9v4M12 17h.01\" />\n    </svg>\n  );\n}\n\nfunction ErrorIcon() {\n  return (\n    <svg\n      viewBox=\"0 0 24 24\"\n      width=\"18\"\n      height=\"18\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth=\"2\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      aria-hidden=\"true\"\n    >\n      <circle cx=\"12\" cy=\"12\" r=\"9\" />\n      <path d=\"m15 9-6 6M9 9l6 6\" />\n    </svg>\n  );\n}\n\nfunction InfoIcon() {\n  return (\n    <svg\n      viewBox=\"0 0 24 24\"\n      width=\"18\"\n      height=\"18\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth=\"2\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      aria-hidden=\"true\"\n    >\n      <circle cx=\"12\" cy=\"12\" r=\"9\" />\n      <path d=\"M12 11v5M12 8h.01\" />\n    </svg>\n  );\n}\n"
    }
  ]
}