{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "sonner",
  "title": "Sonner",
  "type": "registry:ui",
  "description": "Toast notification system — non-blocking, auto-dismissing alerts that stack in a corner. Built on the `sonner` library with the registry’s tokens applied.",
  "categories": [
    "feedback"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/components/sonner/sonner.ts",
      "content": "import { LitElement, css, html, isServer, nothing, type TemplateResult } from 'lit'\nimport { CircleCheck, Info, Loader2, OctagonX, TriangleAlert, X } from 'lucide'\nimport { cn } from '../../lib/utils'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\nimport { icon } from '../../lib/icon'\n\nexport type ToastType = 'default' | 'success' | 'info' | 'warning' | 'error' | 'loading'\nexport type ToastPosition =\n  | 'top-left'\n  | 'top-center'\n  | 'top-right'\n  | 'bottom-left'\n  | 'bottom-center'\n  | 'bottom-right'\n\nexport interface ToastAction {\n  label: string\n  onClick: (e: MouseEvent) => void\n}\n\nexport interface ToastOptions {\n  id?: string | number\n  description?: string\n  duration?: number\n  closeButton?: boolean\n  action?: ToastAction\n  type?: ToastType\n}\n\nexport interface ToastItem extends ToastOptions {\n  id: string | number\n  message: string\n  createdAt: number\n}\n\nlet toastIdCounter = 0\nconst listeners = new Set<(toasts: ToastItem[]) => void>()\nlet currentToasts: ToastItem[] = []\n\nfunction notify() {\n  for (const listener of listeners) {\n    listener([...currentToasts])\n  }\n}\n\nexport function createToast(message: string, options: ToastOptions = {}): string | number {\n  const id = options.id ?? ++toastIdCounter\n  const item: ToastItem = {\n    ...options,\n    id,\n    message,\n    createdAt: Date.now(),\n    type: options.type ?? 'default',\n  }\n  const existingIdx = currentToasts.findIndex((t) => t.id === id)\n  if (existingIdx >= 0) {\n    currentToasts[existingIdx] = item\n  } else {\n    currentToasts.push(item)\n  }\n  notify()\n\n  const dur = options.duration ?? 4000\n  if (dur !== Number.POSITIVE_INFINITY && options.type !== 'loading') {\n    setTimeout(() => {\n      dismissToast(id)\n    }, dur)\n  }\n\n  return id\n}\n\nexport function dismissToast(id: string | number) {\n  currentToasts = currentToasts.filter((t) => t.id !== id)\n  notify()\n}\n\nexport const toast = Object.assign(\n  (message: string, options?: ToastOptions) => createToast(message, options),\n  {\n    success: (message: string, options?: ToastOptions) => createToast(message, { ...options, type: 'success' }),\n    info: (message: string, options?: ToastOptions) => createToast(message, { ...options, type: 'info' }),\n    warning: (message: string, options?: ToastOptions) => createToast(message, { ...options, type: 'warning' }),\n    error: (message: string, options?: ToastOptions) => createToast(message, { ...options, type: 'error' }),\n    loading: (message: string, options?: ToastOptions) => createToast(message, { ...options, type: 'loading' }),\n    dismiss: (id: string | number) => dismissToast(id),\n    promise: <T>(\n      promise: Promise<T>,\n      options: {\n        loading: string\n        success: string | ((data: T) => string)\n        error: string | ((err: unknown) => string)\n      },\n    ) => {\n      const id = createToast(options.loading, { type: 'loading', duration: Number.POSITIVE_INFINITY })\n      promise\n        .then((data) => {\n          const msg = typeof options.success === 'function' ? options.success(data) : options.success\n          createToast(msg, { id, type: 'success', duration: 4000 })\n        })\n        .catch((err) => {\n          const msg = typeof options.error === 'function' ? options.error(err) : options.error\n          createToast(msg, { id, type: 'error', duration: 4000 })\n        })\n      return promise\n    },\n  },\n)\n\nconst trueByDefault = { fromAttribute: (v: string | null) => v !== 'false', toAttribute: () => null }\n\n/**\n * <uip-sonner> — Toast container compatible with sonner.\n */\nexport class UipSonner extends LitElement {\n  static styles = [tailwind, css`:host { display: block; position: fixed; z-index: 9999; pointer-events: none; }`]\n\n  static properties = {\n    position: { type: String },\n    duration: { type: Number },\n    visibleToasts: { type: Number, attribute: 'visible-toasts' },\n    richColors: { attribute: 'rich-colors', converter: trueByDefault },\n    closeButton: { attribute: 'close-button', type: Boolean },\n    toasts: { state: true },\n  }\n\n  position: ToastPosition = 'bottom-right'\n  duration = 4000\n  visibleToasts = 3\n  richColors = true\n  closeButton = false\n\n  private toasts: ToastItem[] = []\n  private unsubscribe?: () => void\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'sonner')\n\n    const update = (t: ToastItem[]) => {\n      this.toasts = t\n    }\n    listeners.add(update)\n    this.unsubscribe = () => listeners.delete(update)\n    this.toasts = [...currentToasts]\n  }\n\n  disconnectedCallback() {\n    super.disconnectedCallback()\n    this.unsubscribe?.()\n  }\n\n  private renderIcon(type: ToastType): TemplateResult | typeof nothing {\n    switch (type) {\n      case 'success':\n        return icon(CircleCheck, 'circle-check', 'size-4 text-emerald-500')\n      case 'info':\n        return icon(Info, 'info', 'size-4 text-blue-500')\n      case 'warning':\n        return icon(TriangleAlert, 'triangle-alert', 'size-4 text-amber-500')\n      case 'error':\n        return icon(OctagonX, 'octagon-x', 'size-4 text-red-500')\n      case 'loading':\n        return icon(Loader2, 'loader-2', 'size-4 animate-spin text-muted-foreground')\n      default:\n        return nothing\n    }\n  }\n\n  render() {\n    const positionClasses: Record<ToastPosition, string> = {\n      'top-left': 'top-4 left-4 flex-col',\n      'top-center': 'top-4 left-1/2 -translate-x-1/2 flex-col items-center',\n      'top-right': 'top-4 right-4 flex-col items-end',\n      'bottom-left': 'bottom-4 left-4 flex-col-reverse',\n      'bottom-center': 'bottom-4 left-1/2 -translate-x-1/2 flex-col-reverse items-center',\n      'bottom-right': 'bottom-4 right-4 flex-col-reverse items-end',\n    }\n\n    const shown = this.toasts.slice(-this.visibleToasts)\n\n    return html`\n      <div\n        part=\"container\"\n        data-slot=\"sonner-container\"\n        class=${cn('fixed flex gap-2 pointer-events-none p-4 z-[9999]', positionClasses[this.position])}\n      >\n        ${shown.map((item) => {\n          const showClose = item.closeButton !== undefined ? item.closeButton : this.closeButton\n          return html`\n            <div\n              part=\"toast\"\n              data-slot=\"sonner-toast\"\n              data-type=${item.type ?? 'default'}\n              class=\"border-border bg-popover text-popover-foreground pointer-events-auto relative flex w-80 items-center justify-between gap-3 rounded-lg border p-4 shadow-lg transition-all animate-in fade-in slide-in-from-bottom-2 duration-200\"\n            >\n              <div class=\"flex items-start gap-3\">\n                ${item.type && item.type !== 'default'\n                  ? html`<span class=\"mt-0.5 shrink-0\">${this.renderIcon(item.type)}</span>`\n                  : nothing}\n                <div class=\"space-y-1\">\n                  <div class=\"text-sm font-medium leading-none\">${item.message}</div>\n                  ${item.description\n                    ? html`<div class=\"text-muted-foreground text-xs leading-relaxed\">${item.description}</div>`\n                    : nothing}\n                </div>\n              </div>\n\n              <div class=\"flex items-center gap-2 shrink-0\">\n                ${item.action\n                  ? html`\n                      <button\n                        type=\"button\"\n                        class=\"bg-primary text-primary-foreground hover:bg-primary/90 rounded-md px-2.5 py-1 text-xs font-medium transition-colors\"\n                        @click=${(e: MouseEvent) => {\n                          item.action?.onClick(e)\n                          dismissToast(item.id)\n                        }}\n                      >\n                        ${item.action.label}\n                      </button>\n                    `\n                  : nothing}\n                ${showClose\n                  ? html`\n                      <button\n                        type=\"button\"\n                        class=\"text-muted-foreground hover:text-foreground inline-flex size-5 items-center justify-center rounded transition-colors\"\n                        aria-label=\"Dismiss toast\"\n                        @click=${() => dismissToast(item.id)}\n                      >\n                        ${icon(X, 'x', 'size-3.5')}\n                      </button>\n                    `\n                  : nothing}\n              </div>\n            </div>\n          `\n        })}\n      </div>\n    `\n  }\n}\n\ncustomElements.get('uip-sonner') || customElements.define('uip-sonner', UipSonner)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-sonner': UipSonner\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/sonner/sonner.ts"
    }
  ],
  "dependencies": [
    "lit",
    "lucide"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/lit-core.json"
  ]
}
