{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "sonner",
  "title": "Sonner",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/sonner/sonner.component.ts",
      "content": "import {\n  AfterViewInit,\n  Component,\n  ElementRef,\n  Input,\n  OnDestroy,\n  OnInit,\n  ViewEncapsulation,\n  computed,\n  effect,\n  inject,\n  signal,\n  untracked,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { DOCUMENT } from '@angular/common'\nimport { cn } from '@/lib/utils'\nimport { ThemeService } from '@/lib/use-theme'\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Toast state + imperative `toast()` API — a port of sonner 2.x (MIT) so Angular\n// apps call the exact same functions React apps import from 'sonner'.\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport type ToastTypes = 'normal' | 'action' | 'success' | 'info' | 'warning' | 'error' | 'loading' | 'default'\nexport type Position = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'top-center' | 'bottom-center'\nexport type SwipeDirection = 'top' | 'right' | 'bottom' | 'left'\nexport type ToasterTheme = 'light' | 'dark' | 'system'\ntype StyleMap = Record<string, string>\n\nexport interface Action {\n  label: string\n  onClick: (event: MouseEvent) => void\n  actionButtonStyle?: StyleMap\n}\n\nexport interface ToastClassnames {\n  toast?: string\n  title?: string\n  description?: string\n  loader?: string\n  closeButton?: string\n  cancelButton?: string\n  actionButton?: string\n  success?: string\n  error?: string\n  info?: string\n  warning?: string\n  loading?: string\n  default?: string\n  content?: string\n  icon?: string\n}\n\nexport interface ExternalToast {\n  id?: number | string\n  toasterId?: string\n  /** `null` hides the type icon (custom icon templates are not supported in the Angular port). */\n  icon?: null\n  richColors?: boolean\n  invert?: boolean\n  closeButton?: boolean\n  dismissible?: boolean\n  description?: string | (() => string)\n  duration?: number\n  action?: Action\n  cancel?: Action\n  onDismiss?: (toast: ToastT) => void\n  onAutoClose?: (toast: ToastT) => void\n  cancelButtonStyle?: StyleMap\n  actionButtonStyle?: StyleMap\n  style?: StyleMap\n  unstyled?: boolean\n  className?: string\n  classNames?: ToastClassnames\n  descriptionClassName?: string\n  position?: Position\n  testId?: string\n}\n\nexport interface ToastT extends ExternalToast {\n  id: number | string\n  title?: string | (() => string)\n  type?: ToastTypes\n  promise?: PromiseLike<unknown> | (() => PromiseLike<unknown>)\n  delete?: boolean\n}\n\ninterface ToastToDismiss {\n  id: number | string\n  dismiss: boolean\n}\n\ntype PromiseResult<T> =\n  | string\n  | (ExternalToast & { message?: string })\n  | ((\n      data: T,\n    ) => string | (ExternalToast & { message?: string }) | Promise<string | (ExternalToast & { message?: string })>)\nexport type PromiseData<T = unknown> = Omit<ExternalToast, 'description'> & {\n  loading?: string\n  success?: PromiseResult<T>\n  error?: PromiseResult<unknown>\n  description?: string | ((data: unknown) => string | Promise<string>)\n  finally?: () => void | Promise<void>\n}\n\nexport interface HeightT {\n  height: number\n  toastId: number | string\n  position?: Position\n}\n\nlet toastsCounter = 1\n\nconst isHttpResponse = (data: unknown): data is Response =>\n  !!data &&\n  typeof data === 'object' &&\n  'ok' in data &&\n  typeof (data as Response).ok === 'boolean' &&\n  'status' in data &&\n  typeof (data as Response).status === 'number'\n\n/** sonner's Observer: the toast queue every <ui-toaster> subscribes to. */\nclass Observer {\n  subscribers: ((toast: ToastT | ToastToDismiss) => void)[] = []\n  toasts: ToastT[] = []\n  dismissedToasts = new Set<number | string>()\n\n  subscribe = (subscriber: (toast: ToastT | ToastToDismiss) => void) => {\n    this.subscribers.push(subscriber)\n    return () => {\n      const index = this.subscribers.indexOf(subscriber)\n      this.subscribers.splice(index, 1)\n    }\n  }\n\n  publish = (data: ToastT) => {\n    this.subscribers.forEach((subscriber) => subscriber(data))\n  }\n\n  addToast = (data: ToastT) => {\n    this.publish(data)\n    this.toasts = [...this.toasts, data]\n  }\n\n  create = (\n    data: ExternalToast & { message?: string | (() => string); type?: ToastTypes; promise?: ToastT['promise'] },\n  ) => {\n    const { message, ...rest } = data\n    const id =\n      typeof data?.id === 'number' || (typeof data?.id === 'string' && data.id.length > 0) ? data.id! : toastsCounter++\n    const alreadyExists = this.toasts.find((toast) => toast.id === id)\n    const dismissible = data.dismissible === undefined ? true : data.dismissible\n    if (this.dismissedToasts.has(id)) this.dismissedToasts.delete(id)\n    if (alreadyExists) {\n      this.toasts = this.toasts.map((toast) => {\n        if (toast.id === id) {\n          this.publish({ ...toast, ...data, id, title: message })\n          return { ...toast, ...data, id, dismissible, title: message }\n        }\n        return toast\n      })\n    } else {\n      this.addToast({ title: message, ...rest, dismissible, id })\n    }\n    return id\n  }\n\n  dismiss = (id?: number | string) => {\n    if (id) {\n      this.dismissedToasts.add(id)\n      requestAnimationFrame(() => this.subscribers.forEach((subscriber) => subscriber({ id, dismiss: true })))\n    } else {\n      this.toasts.forEach((toast) => {\n        this.subscribers.forEach((subscriber) => subscriber({ id: toast.id, dismiss: true }))\n      })\n    }\n    return id\n  }\n\n  message = (message: string | (() => string), data?: ExternalToast) => this.create({ ...data, message })\n  error = (message: string | (() => string), data?: ExternalToast) => this.create({ ...data, message, type: 'error' })\n  success = (message: string | (() => string), data?: ExternalToast) =>\n    this.create({ ...data, type: 'success', message })\n  info = (message: string | (() => string), data?: ExternalToast) => this.create({ ...data, type: 'info', message })\n  warning = (message: string | (() => string), data?: ExternalToast) =>\n    this.create({ ...data, type: 'warning', message })\n  loading = (message: string | (() => string), data?: ExternalToast) =>\n    this.create({ ...data, type: 'loading', message })\n\n  promise = <T>(promise: PromiseLike<T> | (() => PromiseLike<T>), data?: PromiseData<T>) => {\n    if (!data) return\n    let id: number | string | undefined = undefined\n    if (data.loading !== undefined) {\n      id = this.create({\n        ...data,\n        promise,\n        type: 'loading',\n        message: data.loading,\n        description: typeof data.description !== 'function' ? data.description : undefined,\n      } as never)\n    }\n    const p = Promise.resolve(promise instanceof Function ? promise() : promise)\n    let shouldDismiss = id !== undefined\n    let result: ['resolve', T] | ['reject', unknown]\n    const settle = async (type: 'error' | 'success', handler: PromiseResult<never> | undefined, value: unknown) => {\n      shouldDismiss = false\n      const promiseData = typeof handler === 'function' ? await (handler as (v: unknown) => unknown)(value) : handler\n      const description = typeof data.description === 'function' ? await data.description(value) : data.description\n      const toastSettings =\n        typeof promiseData === 'object' && promiseData !== null ? promiseData : { message: promiseData as string }\n      this.create({ id, type, description, ...(toastSettings as object) })\n    }\n    const originalPromise = p\n      .then(async (response) => {\n        result = ['resolve', response]\n        if (isHttpResponse(response) && !response.ok) {\n          await settle('error', data.error as never, `HTTP error! status: ${response.status}`)\n        } else if (response instanceof Error) {\n          await settle('error', data.error as never, response)\n        } else if (data.success !== undefined) {\n          await settle('success', data.success as never, response)\n        }\n      })\n      .catch(async (error) => {\n        result = ['reject', error]\n        if (data.error !== undefined) await settle('error', data.error as never, error)\n      })\n      .finally(() => {\n        if (shouldDismiss) {\n          this.dismiss(id)\n          id = undefined\n        }\n        data.finally?.call(data)\n      })\n    const unwrap = () =>\n      new Promise<T>((resolve, reject) =>\n        originalPromise\n          .then(() => (result[0] === 'reject' ? reject(result[1]) : resolve(result[1] as T)))\n          .catch(reject),\n      )\n    if (typeof id !== 'string' && typeof id !== 'number') return { unwrap }\n    return Object.assign(Object(id) as object, { unwrap })\n  }\n\n  getActiveToasts = () => this.toasts.filter((toast) => !this.dismissedToasts.has(toast.id))\n}\n\nexport const ToastState = new Observer()\n\nconst toastFunction = (message: string | (() => string), data?: ExternalToast) => {\n  const id = data?.id || toastsCounter++\n  ToastState.addToast({ title: message, ...data, id })\n  return id\n}\n\n/**\n * The sonner `toast()` API: `toast('Saved')`, `toast.success(...)`, `.info`, `.warning`,\n * `.error`, `.loading`, `.message`, `.promise(p, { loading, success, error })`,\n * `.dismiss(id?)`, `.getHistory()`, `.getToasts()`. Rendered by every mounted <ui-toaster>.\n */\nexport const toast = Object.assign(\n  toastFunction,\n  {\n    success: ToastState.success,\n    info: ToastState.info,\n    warning: ToastState.warning,\n    error: ToastState.error,\n    message: ToastState.message,\n    promise: ToastState.promise,\n    dismiss: ToastState.dismiss,\n    loading: ToastState.loading,\n  },\n  {\n    getHistory: () => ToastState.toasts,\n    getToasts: () => ToastState.getActiveToasts(),\n  },\n)\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Rendering\n// ─────────────────────────────────────────────────────────────────────────────\n\nconst VISIBLE_TOASTS_AMOUNT = 3\nconst VIEWPORT_OFFSET = '24px'\nconst MOBILE_VIEWPORT_OFFSET = '16px'\nconst TOAST_LIFETIME = 4000\nconst TOAST_WIDTH = 356\nconst GAP = 14\nconst SWIPE_THRESHOLD = 45\nconst TIME_BEFORE_UNMOUNT = 200\n\n/** sonner's stylesheet (dist/styles.css, what the React Toaster injects), copied verbatim. */\nconst SONNER_CSS =\n  \"[data-sonner-toaster][dir=ltr],html[dir=ltr]{--toast-icon-margin-start:-3px;--toast-icon-margin-end:4px;--toast-svg-margin-start:-1px;--toast-svg-margin-end:0px;--toast-button-margin-start:auto;--toast-button-margin-end:0;--toast-close-button-start:0;--toast-close-button-end:unset;--toast-close-button-transform:translate(-35%, -35%)}[data-sonner-toaster][dir=rtl],html[dir=rtl]{--toast-icon-margin-start:4px;--toast-icon-margin-end:-3px;--toast-svg-margin-start:0px;--toast-svg-margin-end:-1px;--toast-button-margin-start:0;--toast-button-margin-end:auto;--toast-close-button-start:unset;--toast-close-button-end:0;--toast-close-button-transform:translate(35%, -35%)}[data-sonner-toaster]{position:fixed;width:var(--width);font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;--gray1:hsl(0, 0%, 99%);--gray2:hsl(0, 0%, 97.3%);--gray3:hsl(0, 0%, 95.1%);--gray4:hsl(0, 0%, 93%);--gray5:hsl(0, 0%, 90.9%);--gray6:hsl(0, 0%, 88.7%);--gray7:hsl(0, 0%, 85.8%);--gray8:hsl(0, 0%, 78%);--gray9:hsl(0, 0%, 56.1%);--gray10:hsl(0, 0%, 52.3%);--gray11:hsl(0, 0%, 43.5%);--gray12:hsl(0, 0%, 9%);--border-radius:8px;box-sizing:border-box;padding:0;margin:0;list-style:none;outline:0;z-index:999999999;transition:transform .4s ease}@media (hover:none) and (pointer:coarse){[data-sonner-toaster][data-lifted=true]{transform:none}}[data-sonner-toaster][data-x-position=right]{right:var(--offset-right)}[data-sonner-toaster][data-x-position=left]{left:var(--offset-left)}[data-sonner-toaster][data-x-position=center]{left:50%;transform:translateX(-50%)}[data-sonner-toaster][data-y-position=top]{top:var(--offset-top)}[data-sonner-toaster][data-y-position=bottom]{bottom:var(--offset-bottom)}[data-sonner-toast]{--y:translateY(100%);--lift-amount:calc(var(--lift) * var(--gap));z-index:var(--z-index);position:absolute;opacity:0;transform:var(--y);touch-action:none;transition:transform .4s,opacity .4s,height .4s,box-shadow .2s;box-sizing:border-box;outline:0;overflow-wrap:anywhere}[data-sonner-toast][data-styled=true]{padding:16px;background:var(--normal-bg);border:1px solid var(--normal-border);color:var(--normal-text);border-radius:var(--border-radius);box-shadow:0 4px 12px rgba(0,0,0,.1);width:var(--width);font-size:13px;display:flex;align-items:center;gap:6px}[data-sonner-toast]:focus-visible{box-shadow:0 4px 12px rgba(0,0,0,.1),0 0 0 2px rgba(0,0,0,.2)}[data-sonner-toast][data-y-position=top]{top:0;--y:translateY(-100%);--lift:1;--lift-amount:calc(1 * var(--gap))}[data-sonner-toast][data-y-position=bottom]{bottom:0;--y:translateY(100%);--lift:-1;--lift-amount:calc(var(--lift) * var(--gap))}[data-sonner-toast][data-styled=true] [data-description]{font-weight:400;line-height:1.4;color:#3f3f3f}[data-rich-colors=true][data-sonner-toast][data-styled=true] [data-description]{color:inherit}[data-sonner-toaster][data-sonner-theme=dark] [data-description]{color:#e8e8e8}[data-sonner-toast][data-styled=true] [data-title]{font-weight:500;line-height:1.5;color:inherit}[data-sonner-toast][data-styled=true] [data-icon]{display:flex;height:16px;width:16px;position:relative;justify-content:flex-start;align-items:center;flex-shrink:0;margin-left:var(--toast-icon-margin-start);margin-right:var(--toast-icon-margin-end)}[data-sonner-toast][data-promise=true] [data-icon]>svg{opacity:0;transform:scale(.8);transform-origin:center;animation:sonner-fade-in .3s ease forwards}[data-sonner-toast][data-styled=true] [data-icon]>*{flex-shrink:0}[data-sonner-toast][data-styled=true] [data-icon] svg{margin-left:var(--toast-svg-margin-start);margin-right:var(--toast-svg-margin-end)}[data-sonner-toast][data-styled=true] [data-content]{display:flex;flex-direction:column;gap:2px}[data-sonner-toast][data-styled=true] [data-button]{border-radius:4px;padding-left:8px;padding-right:8px;height:24px;font-size:12px;color:var(--normal-bg);background:var(--normal-text);margin-left:var(--toast-button-margin-start);margin-right:var(--toast-button-margin-end);border:none;font-weight:500;cursor:pointer;outline:0;display:flex;align-items:center;flex-shrink:0;transition:opacity .4s,box-shadow .2s}[data-sonner-toast][data-styled=true] [data-button]:focus-visible{box-shadow:0 0 0 2px rgba(0,0,0,.4)}[data-sonner-toast][data-styled=true] [data-button]:first-of-type{margin-left:var(--toast-button-margin-start);margin-right:var(--toast-button-margin-end)}[data-sonner-toast][data-styled=true] [data-cancel]{color:var(--normal-text);background:rgba(0,0,0,.08)}[data-sonner-toaster][data-sonner-theme=dark] [data-sonner-toast][data-styled=true] [data-cancel]{background:rgba(255,255,255,.3)}[data-sonner-toast][data-styled=true] [data-close-button]{position:absolute;left:var(--toast-close-button-start);right:var(--toast-close-button-end);top:0;height:20px;width:20px;display:flex;justify-content:center;align-items:center;padding:0;color:var(--gray12);background:var(--normal-bg);border:1px solid var(--gray4);transform:var(--toast-close-button-transform);border-radius:50%;cursor:pointer;z-index:1;transition:opacity .1s,background .2s,border-color .2s}[data-sonner-toast][data-styled=true] [data-close-button]:focus-visible{box-shadow:0 4px 12px rgba(0,0,0,.1),0 0 0 2px rgba(0,0,0,.2)}[data-sonner-toast][data-styled=true] [data-disabled=true]{cursor:not-allowed}[data-sonner-toast][data-styled=true]:hover [data-close-button]:hover{background:var(--gray2);border-color:var(--gray5)}[data-sonner-toast][data-swiping=true]::before{content:'';position:absolute;left:-100%;right:-100%;height:100%;z-index:-1}[data-sonner-toast][data-y-position=top][data-swiping=true]::before{bottom:50%;transform:scaleY(3) translateY(50%)}[data-sonner-toast][data-y-position=bottom][data-swiping=true]::before{top:50%;transform:scaleY(3) translateY(-50%)}[data-sonner-toast][data-swiping=false][data-removed=true]::before{content:'';position:absolute;inset:0;transform:scaleY(2)}[data-sonner-toast][data-expanded=true]::after{content:'';position:absolute;left:0;height:calc(var(--gap) + 1px);bottom:100%;width:100%}[data-sonner-toast][data-mounted=true]{--y:translateY(0);opacity:1}[data-sonner-toast][data-expanded=false][data-front=false]{--scale:var(--toasts-before) * 0.05 + 1;--y:translateY(calc(var(--lift-amount) * var(--toasts-before))) scale(calc(-1 * var(--scale)));height:var(--front-toast-height)}[data-sonner-toast]>*{transition:opacity .4s}[data-sonner-toast][data-x-position=right]{right:0}[data-sonner-toast][data-x-position=left]{left:0}[data-sonner-toast][data-expanded=false][data-front=false][data-styled=true]>*{opacity:0}[data-sonner-toast][data-visible=false]{opacity:0;pointer-events:none}[data-sonner-toast][data-mounted=true][data-expanded=true]{--y:translateY(calc(var(--lift) * var(--offset)));height:var(--initial-height)}[data-sonner-toast][data-removed=true][data-front=true][data-swipe-out=false]{--y:translateY(calc(var(--lift) * -100%));opacity:0}[data-sonner-toast][data-removed=true][data-front=false][data-swipe-out=false][data-expanded=true]{--y:translateY(calc(var(--lift) * var(--offset) + var(--lift) * -100%));opacity:0}[data-sonner-toast][data-removed=true][data-front=false][data-swipe-out=false][data-expanded=false]{--y:translateY(40%);opacity:0;transition:transform .5s,opacity .2s}[data-sonner-toast][data-removed=true][data-front=false]::before{height:calc(var(--initial-height) + 20%)}[data-sonner-toast][data-swiping=true]{transform:var(--y) translateY(var(--swipe-amount-y,0)) translateX(var(--swipe-amount-x,0));transition:none}[data-sonner-toast][data-swiped=true]{user-select:none}[data-sonner-toast][data-swipe-out=true][data-y-position=bottom],[data-sonner-toast][data-swipe-out=true][data-y-position=top]{animation-duration:.2s;animation-timing-function:ease-out;animation-fill-mode:forwards}[data-sonner-toast][data-swipe-out=true][data-swipe-direction=left]{animation-name:swipe-out-left}[data-sonner-toast][data-swipe-out=true][data-swipe-direction=right]{animation-name:swipe-out-right}[data-sonner-toast][data-swipe-out=true][data-swipe-direction=up]{animation-name:swipe-out-up}[data-sonner-toast][data-swipe-out=true][data-swipe-direction=down]{animation-name:swipe-out-down}@keyframes swipe-out-left{from{transform:var(--y) translateX(var(--swipe-amount-x));opacity:1}to{transform:var(--y) translateX(calc(var(--swipe-amount-x) - 100%));opacity:0}}@keyframes swipe-out-right{from{transform:var(--y) translateX(var(--swipe-amount-x));opacity:1}to{transform:var(--y) translateX(calc(var(--swipe-amount-x) + 100%));opacity:0}}@keyframes swipe-out-up{from{transform:var(--y) translateY(var(--swipe-amount-y));opacity:1}to{transform:var(--y) translateY(calc(var(--swipe-amount-y) - 100%));opacity:0}}@keyframes swipe-out-down{from{transform:var(--y) translateY(var(--swipe-amount-y));opacity:1}to{transform:var(--y) translateY(calc(var(--swipe-amount-y) + 100%));opacity:0}}@media (max-width:600px){[data-sonner-toaster]{position:fixed;right:var(--mobile-offset-right);left:var(--mobile-offset-left);width:100%}[data-sonner-toaster][dir=rtl]{left:calc(var(--mobile-offset-left) * -1)}[data-sonner-toaster] [data-sonner-toast]{left:0;right:0;width:calc(100% - var(--mobile-offset-left) * 2)}[data-sonner-toaster][data-x-position=left]{left:var(--mobile-offset-left)}[data-sonner-toaster][data-y-position=bottom]{bottom:var(--mobile-offset-bottom)}[data-sonner-toaster][data-y-position=top]{top:var(--mobile-offset-top)}[data-sonner-toaster][data-x-position=center]{left:var(--mobile-offset-left);right:var(--mobile-offset-right);transform:none}}[data-sonner-toaster][data-sonner-theme=light]{--normal-bg:#fff;--normal-border:var(--gray4);--normal-text:var(--gray12);--success-bg:hsl(143, 85%, 96%);--success-border:hsl(145, 92%, 87%);--success-text:hsl(140, 100%, 27%);--info-bg:hsl(208, 100%, 97%);--info-border:hsl(221, 91%, 93%);--info-text:hsl(210, 92%, 45%);--warning-bg:hsl(49, 100%, 97%);--warning-border:hsl(49, 91%, 84%);--warning-text:hsl(31, 92%, 45%);--error-bg:hsl(359, 100%, 97%);--error-border:hsl(359, 100%, 94%);--error-text:hsl(360, 100%, 45%)}[data-sonner-toaster][data-sonner-theme=light] [data-sonner-toast][data-invert=true]{--normal-bg:#000;--normal-border:hsl(0, 0%, 20%);--normal-text:var(--gray1)}[data-sonner-toaster][data-sonner-theme=dark] [data-sonner-toast][data-invert=true]{--normal-bg:#fff;--normal-border:var(--gray3);--normal-text:var(--gray12)}[data-sonner-toaster][data-sonner-theme=dark]{--normal-bg:#000;--normal-bg-hover:hsl(0, 0%, 12%);--normal-border:hsl(0, 0%, 20%);--normal-border-hover:hsl(0, 0%, 25%);--normal-text:var(--gray1);--success-bg:hsl(150, 100%, 6%);--success-border:hsl(147, 100%, 12%);--success-text:hsl(150, 86%, 65%);--info-bg:hsl(215, 100%, 6%);--info-border:hsl(223, 43%, 17%);--info-text:hsl(216, 87%, 65%);--warning-bg:hsl(64, 100%, 6%);--warning-border:hsl(60, 100%, 9%);--warning-text:hsl(46, 87%, 65%);--error-bg:hsl(358, 76%, 10%);--error-border:hsl(357, 89%, 16%);--error-text:hsl(358, 100%, 81%)}[data-sonner-toaster][data-sonner-theme=dark] [data-sonner-toast] [data-close-button]{background:var(--normal-bg);border-color:var(--normal-border);color:var(--normal-text)}[data-sonner-toaster][data-sonner-theme=dark] [data-sonner-toast] [data-close-button]:hover{background:var(--normal-bg-hover);border-color:var(--normal-border-hover)}[data-rich-colors=true][data-sonner-toast][data-type=success]{background:var(--success-bg);border-color:var(--success-border);color:var(--success-text)}[data-rich-colors=true][data-sonner-toast][data-type=success] [data-close-button]{background:var(--success-bg);border-color:var(--success-border);color:var(--success-text)}[data-rich-colors=true][data-sonner-toast][data-type=info]{background:var(--info-bg);border-color:var(--info-border);color:var(--info-text)}[data-rich-colors=true][data-sonner-toast][data-type=info] [data-close-button]{background:var(--info-bg);border-color:var(--info-border);color:var(--info-text)}[data-rich-colors=true][data-sonner-toast][data-type=warning]{background:var(--warning-bg);border-color:var(--warning-border);color:var(--warning-text)}[data-rich-colors=true][data-sonner-toast][data-type=warning] [data-close-button]{background:var(--warning-bg);border-color:var(--warning-border);color:var(--warning-text)}[data-rich-colors=true][data-sonner-toast][data-type=error]{background:var(--error-bg);border-color:var(--error-border);color:var(--error-text)}[data-rich-colors=true][data-sonner-toast][data-type=error] [data-close-button]{background:var(--error-bg);border-color:var(--error-border);color:var(--error-text)}.sonner-loading-wrapper{--size:16px;height:var(--size);width:var(--size);position:absolute;inset:0;z-index:10}.sonner-loading-wrapper[data-visible=false]{transform-origin:center;animation:sonner-fade-out .2s ease forwards}.sonner-spinner{position:relative;top:50%;left:50%;height:var(--size);width:var(--size)}.sonner-loading-bar{animation:sonner-spin 1.2s linear infinite;background:var(--gray11);border-radius:6px;height:8%;left:-10%;position:absolute;top:-3.9%;width:24%}.sonner-loading-bar:first-child{animation-delay:-1.2s;transform:rotate(.0001deg) translate(146%)}.sonner-loading-bar:nth-child(2){animation-delay:-1.1s;transform:rotate(30deg) translate(146%)}.sonner-loading-bar:nth-child(3){animation-delay:-1s;transform:rotate(60deg) translate(146%)}.sonner-loading-bar:nth-child(4){animation-delay:-.9s;transform:rotate(90deg) translate(146%)}.sonner-loading-bar:nth-child(5){animation-delay:-.8s;transform:rotate(120deg) translate(146%)}.sonner-loading-bar:nth-child(6){animation-delay:-.7s;transform:rotate(150deg) translate(146%)}.sonner-loading-bar:nth-child(7){animation-delay:-.6s;transform:rotate(180deg) translate(146%)}.sonner-loading-bar:nth-child(8){animation-delay:-.5s;transform:rotate(210deg) translate(146%)}.sonner-loading-bar:nth-child(9){animation-delay:-.4s;transform:rotate(240deg) translate(146%)}.sonner-loading-bar:nth-child(10){animation-delay:-.3s;transform:rotate(270deg) translate(146%)}.sonner-loading-bar:nth-child(11){animation-delay:-.2s;transform:rotate(300deg) translate(146%)}.sonner-loading-bar:nth-child(12){animation-delay:-.1s;transform:rotate(330deg) translate(146%)}@keyframes sonner-fade-in{0%{opacity:0;transform:scale(.8)}100%{opacity:1;transform:scale(1)}}@keyframes sonner-fade-out{0%{opacity:1;transform:scale(1)}100%{opacity:0;transform:scale(.8)}}@keyframes sonner-spin{0%{opacity:1}100%{opacity:.15}}@media (prefers-reduced-motion){.sonner-loading-bar,[data-sonner-toast],[data-sonner-toast]>*{transition:none!important;animation:none!important}}.sonner-loader{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);transform-origin:center;transition:opacity .2s,transform .2s}.sonner-loader[data-visible=false]{opacity:0;transform:scale(.8) translate(-50%,-50%)}\"\n\ntype Offset =\n  { top?: string | number; right?: string | number; bottom?: string | number; left?: string | number } | string | number\n\nfunction assignOffset(defaultOffset?: Offset, mobileOffset?: Offset): StyleMap {\n  const styles: StyleMap = {}\n  ;[defaultOffset, mobileOffset].forEach((offset, index) => {\n    const isMobile = index === 1\n    const prefix = isMobile ? '--mobile-offset' : '--offset'\n    const defaultValue = isMobile ? MOBILE_VIEWPORT_OFFSET : VIEWPORT_OFFSET\n    const assignAll = (o: string | number) => {\n      for (const key of ['top', 'right', 'bottom', 'left'])\n        styles[`${prefix}-${key}`] = typeof o === 'number' ? `${o}px` : o\n    }\n    if (typeof offset === 'number' || typeof offset === 'string') assignAll(offset)\n    else if (typeof offset === 'object') {\n      for (const key of ['top', 'right', 'bottom', 'left'] as const) {\n        const v = offset[key]\n        styles[`${prefix}-${key}`] = v === undefined ? defaultValue : typeof v === 'number' ? `${v}px` : v\n      }\n    } else assignAll(defaultValue)\n  })\n  return styles\n}\n\nfunction getDocumentDirection(): 'ltr' | 'rtl' {\n  if (typeof window === 'undefined' || typeof document === 'undefined') return 'ltr'\n  const dirAttribute = document.documentElement.getAttribute('dir')\n  if (dirAttribute === 'auto' || !dirAttribute)\n    return window.getComputedStyle(document.documentElement).direction as 'ltr' | 'rtl'\n  return dirAttribute as 'ltr' | 'rtl'\n}\n\nfunction defaultSwipeDirections(position: string): SwipeDirection[] {\n  const [y, x] = position.split('-')\n  const directions: SwipeDirection[] = []\n  if (y) directions.push(y as SwipeDirection)\n  if (x) directions.push(x as SwipeDirection)\n  return directions\n}\n\nconst str = (v: boolean | undefined) => (v === undefined ? null : String(v))\n\nexport interface ToastOptions {\n  className?: string\n  closeButton?: boolean\n  descriptionClassName?: string\n  style?: StyleMap\n  cancelButtonStyle?: StyleMap\n  actionButtonStyle?: StyleMap\n  duration?: number\n  unstyled?: boolean\n  classNames?: ToastClassnames\n  closeButtonAriaLabel?: string\n}\n\n/** One toast (sonner's <Toast>): the <li data-sonner-toast>. Internal to the Toaster. */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'li[ui-sonner-toast]',\n  standalone: true,\n  host: {\n    tabindex: '0',\n    'data-sonner-toast': '',\n    '[attr.data-rich-colors]': 'str(t().richColors ?? toaster.richColors)',\n    '[attr.data-styled]': 'String(!(t().unstyled || toaster.toastOptions?.unstyled))',\n    '[attr.data-mounted]': 'String(mounted())',\n    '[attr.data-promise]': 'String(!!t().promise)',\n    '[attr.data-swiped]': 'String(isSwiped())',\n    '[attr.data-removed]': 'String(removed())',\n    '[attr.data-visible]': 'String(index + 1 <= toaster.visibleToasts)',\n    '[attr.data-y-position]': 'y',\n    '[attr.data-x-position]': 'x',\n    '[attr.data-index]': 'index',\n    '[attr.data-front]': 'String(index === 0)',\n    '[attr.data-swiping]': 'String(swiping())',\n    '[attr.data-dismissible]': 'String(dismissible)',\n    '[attr.data-type]': 't().type ?? null',\n    '[attr.data-invert]': 'str(t().invert || toaster.invert)',\n    '[attr.data-swipe-out]': 'String(swipeOut())',\n    '[attr.data-swipe-direction]': 'swipeOutDirection()',\n    '[attr.data-expanded]': 'String(toaster.expanded() || (toaster.expand && mounted()))',\n    '[attr.data-testid]': 't().testId ?? null',\n    '[class]': 'toastClass',\n    '[style]': 'toastStyle()',\n    '(dragend)': 'onDragEnd()',\n    '(pointerdown)': 'onPointerDown($event)',\n    '(pointerup)': 'onPointerUp()',\n    '(pointermove)': 'onPointerMove($event)',\n  },\n  template: `\n    @if (closeButton && t().type !== 'loading') {\n      <button\n        [attr.aria-label]=\"toaster.toastOptions?.closeButtonAriaLabel ?? 'Close toast'\"\n        [attr.data-disabled]=\"String(disabled)\"\n        data-close-button=\"true\"\n        [class]=\"cls('closeButton')\"\n        (click)=\"onCloseClick()\"\n      >\n        <svg\n          xmlns=\"http://www.w3.org/2000/svg\"\n          width=\"24\"\n          height=\"24\"\n          viewBox=\"0 0 24 24\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          stroke-width=\"2\"\n          stroke-linecap=\"round\"\n          stroke-linejoin=\"round\"\n          class=\"lucide lucide-x size-4\"\n          aria-hidden=\"true\"\n        >\n          <path d=\"M18 6 6 18\" />\n          <path d=\"m6 6 12 12\" />\n        </svg>\n      </button>\n    }\n    @if (showIcon) {\n      <div data-icon=\"\" [class]=\"cls('icon')\">\n        @if (t().promise || (t().type === 'loading' && !t().icon)) {\n          <div [class]=\"loaderClass\" [attr.data-visible]=\"String(t().type === 'loading')\">\n            <svg\n              xmlns=\"http://www.w3.org/2000/svg\"\n              width=\"24\"\n              height=\"24\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              stroke-width=\"2\"\n              stroke-linecap=\"round\"\n              stroke-linejoin=\"round\"\n              class=\"lucide lucide-loader-circle size-4 motion-safe:animate-spin\"\n              aria-hidden=\"true\"\n            >\n              <path d=\"M21 12a9 9 0 1 1-6.219-8.56\" />\n            </svg>\n          </div>\n        }\n        @if (t().type !== 'loading') {\n          @switch (t().type) {\n            @case ('success') {\n              <svg\n                xmlns=\"http://www.w3.org/2000/svg\"\n                width=\"24\"\n                height=\"24\"\n                viewBox=\"0 0 24 24\"\n                fill=\"none\"\n                stroke=\"currentColor\"\n                stroke-width=\"2\"\n                stroke-linecap=\"round\"\n                stroke-linejoin=\"round\"\n                class=\"lucide lucide-circle-check size-4\"\n                aria-hidden=\"true\"\n              >\n                <circle cx=\"12\" cy=\"12\" r=\"10\" />\n                <path d=\"m9 12 2 2 4-4\" />\n              </svg>\n            }\n            @case ('info') {\n              <svg\n                xmlns=\"http://www.w3.org/2000/svg\"\n                width=\"24\"\n                height=\"24\"\n                viewBox=\"0 0 24 24\"\n                fill=\"none\"\n                stroke=\"currentColor\"\n                stroke-width=\"2\"\n                stroke-linecap=\"round\"\n                stroke-linejoin=\"round\"\n                class=\"lucide lucide-info size-4\"\n                aria-hidden=\"true\"\n              >\n                <circle cx=\"12\" cy=\"12\" r=\"10\" />\n                <path d=\"M12 16v-4\" />\n                <path d=\"M12 8h.01\" />\n              </svg>\n            }\n            @case ('warning') {\n              <svg\n                xmlns=\"http://www.w3.org/2000/svg\"\n                width=\"24\"\n                height=\"24\"\n                viewBox=\"0 0 24 24\"\n                fill=\"none\"\n                stroke=\"currentColor\"\n                stroke-width=\"2\"\n                stroke-linecap=\"round\"\n                stroke-linejoin=\"round\"\n                class=\"lucide lucide-triangle-alert size-4\"\n                aria-hidden=\"true\"\n              >\n                <path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\" />\n                <path d=\"M12 9v4\" />\n                <path d=\"M12 17h.01\" />\n              </svg>\n            }\n            @case ('error') {\n              <svg\n                xmlns=\"http://www.w3.org/2000/svg\"\n                width=\"24\"\n                height=\"24\"\n                viewBox=\"0 0 24 24\"\n                fill=\"none\"\n                stroke=\"currentColor\"\n                stroke-width=\"2\"\n                stroke-linecap=\"round\"\n                stroke-linejoin=\"round\"\n                class=\"lucide lucide-octagon-x size-4\"\n                aria-hidden=\"true\"\n              >\n                <path d=\"m15 9-6 6\" />\n                <path\n                  d=\"M2.586 16.726A2 2 0 0 1 2 15.312V8.688a2 2 0 0 1 .586-1.414l4.688-4.688A2 2 0 0 1 8.688 2h6.624a2 2 0 0 1 1.414.586l4.688 4.688A2 2 0 0 1 22 8.688v6.624a2 2 0 0 1-.586 1.414l-4.688 4.688a2 2 0 0 1-1.414.586H8.688a2 2 0 0 1-1.414-.586z\"\n                />\n                <path d=\"m9 9 6 6\" />\n              </svg>\n            }\n          }\n        }\n      </div>\n    }\n    <div data-content=\"\" [class]=\"cls('content')\">\n      <div data-title=\"\" [class]=\"cls('title')\">{{ title }}</div>\n      @if (t().description) {\n        <div data-description=\"\" [class]=\"descriptionClass\">{{ description }}</div>\n      }\n    </div>\n    @if (t().cancel; as cancel) {\n      <button\n        data-button=\"true\"\n        data-cancel=\"true\"\n        [style]=\"t().cancelButtonStyle || toaster.toastOptions?.cancelButtonStyle || null\"\n        [class]=\"cls('cancelButton')\"\n        (click)=\"onCancel($event)\"\n      >\n        {{ cancel.label }}\n      </button>\n    }\n    @if (t().action; as action) {\n      <button\n        data-button=\"true\"\n        data-action=\"true\"\n        [style]=\"t().actionButtonStyle || toaster.toastOptions?.actionButtonStyle || null\"\n        [class]=\"cls('actionButton')\"\n        (click)=\"onAction($event)\"\n      >\n        {{ action.label }}\n      </button>\n    }\n  `,\n})\nexport class UiSonnerToastComponent implements AfterViewInit, OnDestroy {\n  readonly toaster = inject(UiToasterComponent)\n  private readonly el = inject<ElementRef<HTMLLIElement>>(ElementRef).nativeElement\n  readonly str = str\n  readonly String = String\n\n  readonly t = signal<ToastT>({ id: 0 })\n  @Input({ required: true })\n  set toast(value: ToastT) {\n    const prev = this.t()\n    this.t.set(value)\n    if (\n      this.mounted() &&\n      (prev.title !== value.title ||\n        prev.description !== value.description ||\n        prev.action !== value.action ||\n        prev.cancel !== value.cancel)\n    ) {\n      requestAnimationFrame(() => this.remeasure())\n    }\n  }\n  @Input() index = 0\n  @Input() position: Position = 'bottom-right'\n\n  readonly mounted = signal(false)\n  readonly removed = signal(false)\n  readonly swiping = signal(false)\n  readonly swipeOut = signal(false)\n  readonly isSwiped = signal(false)\n  readonly swipeOutDirection = signal<'left' | 'right' | 'up' | 'down' | null>(null)\n  private readonly offsetBeforeRemove = signal(0)\n  private readonly initialHeight = signal(0)\n  private swipeDirection: 'x' | 'y' | null = null\n  private pointerStart: { x: number; y: number } | null = null\n  private dragStartTime: Date | null = null\n  private remainingTime = TOAST_LIFETIME\n  private lastDuration = 0\n  private closeTimerStartTime = 0\n  private lastCloseTimerStartTime = 0\n  private deleting = false\n\n  get y(): string {\n    return this.position.split('-')[0]!\n  }\n  get x(): string {\n    return this.position.split('-')[1]!\n  }\n  get dismissible(): boolean {\n    return this.t().dismissible !== false\n  }\n  get disabled(): boolean {\n    return this.t().type === 'loading'\n  }\n  get closeButton(): boolean {\n    return this.t().closeButton ?? this.toaster.toastOptions?.closeButton ?? this.toaster.closeButton\n  }\n  private get duration(): number {\n    return this.t().duration || this.toaster.toastOptions?.duration || this.toaster.duration || TOAST_LIFETIME\n  }\n\n  get showIcon(): boolean {\n    const t = this.t()\n    return !!(t.type || t.icon || t.promise) && t.icon !== null\n  }\n  get title(): string {\n    const title = this.t().title\n    return typeof title === 'function' ? title() : (title ?? '')\n  }\n  get description(): string {\n    const d = this.t().description\n    return typeof d === 'function' ? d() : (d ?? '')\n  }\n\n  cls(part: keyof ToastClassnames): string {\n    return cn(this.toaster.toastOptions?.classNames?.[part], this.t().classNames?.[part])\n  }\n  get loaderClass(): string {\n    return cn(this.toaster.toastOptions?.classNames?.loader, this.t().classNames?.loader, 'sonner-loader')\n  }\n  get descriptionClass(): string {\n    return cn(\n      this.toaster.toastOptions?.descriptionClassName,\n      this.t().descriptionClassName,\n      this.toaster.toastOptions?.classNames?.description,\n      this.t().classNames?.description,\n    )\n  }\n  get toastClass(): string {\n    const t = this.t()\n    const type = t.type as keyof ToastClassnames | undefined\n    const o = this.toaster.toastOptions?.classNames\n    return cn(\n      this.toaster.toastOptions?.className,\n      t.className,\n      o?.toast,\n      t.classNames?.toast,\n      o?.default,\n      type && o?.[type],\n      type && t.classNames?.[type],\n    )\n  }\n\n  // sonner layout math: offset = (index in heights) * gap + heights of newer toasts.\n  private readonly heights = computed(() => this.toaster.heights().filter((h) => h.position == this.t().position))\n  private readonly heightIndex = computed(() => this.heights().findIndex((h) => h.toastId === this.t().id) || 0)\n  readonly offset = computed(() => {\n    const idx = this.heightIndex()\n    const before = this.heights().reduce((prev, curr, i) => (i >= idx ? prev : prev + curr.height), 0)\n    return idx * this.toaster.gap + before\n  })\n\n  toastStyle(): StyleMap {\n    const siblings = this.toaster.filteredToasts().filter((t) => t.position == this.t().position)\n    return {\n      '--index': String(this.index),\n      '--toasts-before': String(this.index),\n      '--z-index': String(siblings.length - this.index),\n      '--offset': `${this.removed() ? this.offsetBeforeRemove() : this.offset()}px`,\n      '--initial-height': this.toaster.expand ? 'auto' : `${this.initialHeight()}px`,\n      ...this.toaster.toastOptions?.style,\n      ...this.t().style,\n    }\n  }\n\n  constructor() {\n    // Auto-dismiss timer: pauses while the stack is expanded (hover), being interacted with, or the tab is hidden.\n    effect((onCleanup) => {\n      const t = this.t()\n      const paused = this.toaster.expanded() || this.toaster.interacting() || this.toaster.documentHidden()\n      const duration = this.duration\n      if (duration !== this.lastDuration) {\n        this.lastDuration = duration\n        this.remainingTime = duration\n      }\n      if ((t.promise && t.type === 'loading') || t.duration === Infinity || t.type === 'loading') return\n      let timeoutId: ReturnType<typeof setTimeout> | undefined\n      if (paused) {\n        if (this.lastCloseTimerStartTime < this.closeTimerStartTime) {\n          this.remainingTime = this.remainingTime - (Date.now() - this.closeTimerStartTime)\n        }\n        this.lastCloseTimerStartTime = Date.now()\n      } else if (this.remainingTime !== Infinity) {\n        this.closeTimerStartTime = Date.now()\n        timeoutId = setTimeout(() => {\n          t.onAutoClose?.(t)\n          this.deleteToast()\n        }, this.remainingTime)\n      }\n      onCleanup(() => clearTimeout(timeoutId))\n    })\n    // toast.dismiss(id) marks the toast `delete`: animate out, then notify.\n    effect(() => {\n      const t = this.t()\n      if (!t.delete) return\n      untracked(() => {\n        this.deleteToast()\n        t.onDismiss?.(t)\n      })\n    })\n  }\n\n  ngAfterViewInit(): void {\n    const height = this.el.getBoundingClientRect().height\n    this.initialHeight.set(height)\n    const t = this.t()\n    this.toaster.heights.update((h) => [{ toastId: t.id, height, position: t.position }, ...h])\n    // Trigger the enter transition (sonner flips data-mounted after first paint).\n    requestAnimationFrame(() => this.mounted.set(true))\n  }\n\n  private remeasure(): void {\n    if (!this.el.isConnected) return\n    const original = this.el.style.height\n    this.el.style.height = 'auto'\n    const newHeight = this.el.getBoundingClientRect().height\n    this.el.style.height = original\n    this.initialHeight.set(newHeight)\n    const t = this.t()\n    this.toaster.heights.update((heights) =>\n      heights.find((h) => h.toastId === t.id)\n        ? heights.map((h) => (h.toastId === t.id ? { ...h, height: newHeight } : h))\n        : [{ toastId: t.id, height: newHeight, position: t.position }, ...heights],\n    )\n  }\n\n  deleteToast(): void {\n    if (this.deleting) return\n    this.deleting = true\n    this.removed.set(true)\n    this.offsetBeforeRemove.set(this.offset())\n    const t = this.t()\n    this.toaster.heights.update((h) => h.filter((x) => x.toastId !== t.id))\n    setTimeout(() => this.toaster.removeToast(t), TIME_BEFORE_UNMOUNT)\n  }\n\n  onCloseClick(): void {\n    if (this.disabled || !this.dismissible) return\n    this.deleteToast()\n    this.t().onDismiss?.(this.t())\n  }\n\n  onCancel(event: MouseEvent): void {\n    const cancel = this.t().cancel\n    if (!cancel || !this.dismissible) return\n    cancel.onClick?.(event)\n    this.deleteToast()\n  }\n\n  onAction(event: MouseEvent): void {\n    const action = this.t().action\n    if (!action) return\n    action.onClick?.(event)\n    if (event.defaultPrevented) return\n    this.deleteToast()\n  }\n\n  onDragEnd(): void {\n    this.swiping.set(false)\n    this.swipeDirection = null\n    this.pointerStart = null\n  }\n\n  onPointerDown(event: PointerEvent): void {\n    if (event.button === 2) return\n    if (this.disabled || !this.dismissible) return\n    this.dragStartTime = new Date()\n    this.offsetBeforeRemove.set(this.offset())\n    const target = event.target as HTMLElement\n    target.setPointerCapture?.(event.pointerId)\n    if (target.tagName === 'BUTTON') return\n    this.swiping.set(true)\n    this.pointerStart = { x: event.clientX, y: event.clientY }\n  }\n\n  onPointerUp(): void {\n    if (this.swipeOut() || !this.dismissible) return\n    this.pointerStart = null\n    const amountX = Number(this.el.style.getPropertyValue('--swipe-amount-x').replace('px', '') || 0)\n    const amountY = Number(this.el.style.getPropertyValue('--swipe-amount-y').replace('px', '') || 0)\n    const timeTaken = Date.now() - (this.dragStartTime?.getTime() ?? Date.now())\n    const amount = this.swipeDirection === 'x' ? amountX : amountY\n    const velocity = Math.abs(amount) / timeTaken\n    if (Math.abs(amount) >= SWIPE_THRESHOLD || velocity > 0.11) {\n      this.offsetBeforeRemove.set(this.offset())\n      this.t().onDismiss?.(this.t())\n      if (this.swipeDirection === 'x') this.swipeOutDirection.set(amountX > 0 ? 'right' : 'left')\n      else this.swipeOutDirection.set(amountY > 0 ? 'down' : 'up')\n      this.deleteToast()\n      this.swipeOut.set(true)\n      return\n    }\n    this.el.style.setProperty('--swipe-amount-x', '0px')\n    this.el.style.setProperty('--swipe-amount-y', '0px')\n    this.isSwiped.set(false)\n    this.swiping.set(false)\n    this.swipeDirection = null\n  }\n\n  onPointerMove(event: PointerEvent): void {\n    if (!this.pointerStart || !this.dismissible) return\n    if ((window.getSelection()?.toString().length ?? 0) > 0) return\n    const yDelta = event.clientY - this.pointerStart.y\n    const xDelta = event.clientX - this.pointerStart.x\n    const directions = this.toaster.swipeDirections ?? defaultSwipeDirections(this.position)\n    if (!this.swipeDirection && (Math.abs(xDelta) > 1 || Math.abs(yDelta) > 1)) {\n      this.swipeDirection = Math.abs(xDelta) > Math.abs(yDelta) ? 'x' : 'y'\n    }\n    const amount = { x: 0, y: 0 }\n    const dampening = (delta: number) => 1 / (1.5 + Math.abs(delta) / 20)\n    if (this.swipeDirection === 'y') {\n      if (directions.includes('top') || directions.includes('bottom')) {\n        if ((directions.includes('top') && yDelta < 0) || (directions.includes('bottom') && yDelta > 0))\n          amount.y = yDelta\n        else {\n          const damped = yDelta * dampening(yDelta)\n          amount.y = Math.abs(damped) < Math.abs(yDelta) ? damped : yDelta\n        }\n      }\n    } else if (this.swipeDirection === 'x') {\n      if (directions.includes('left') || directions.includes('right')) {\n        if ((directions.includes('left') && xDelta < 0) || (directions.includes('right') && xDelta > 0))\n          amount.x = xDelta\n        else {\n          const damped = xDelta * dampening(xDelta)\n          amount.x = Math.abs(damped) < Math.abs(xDelta) ? damped : xDelta\n        }\n      }\n    }\n    if (Math.abs(amount.x) > 0 || Math.abs(amount.y) > 0) this.isSwiped.set(true)\n    this.el.style.setProperty('--swipe-amount-x', `${amount.x}px`)\n    this.el.style.setProperty('--swipe-amount-y', `${amount.y}px`)\n  }\n\n  ngOnDestroy(): void {\n    const id = this.t().id\n    this.toaster.heights.update((h) => h.filter((x) => x.toastId !== id))\n  }\n}\n\n/**\n * Angular port of UIPKGE Sonner — the React registry's <Toaster> (sonner 2.x) with the\n * same defaults: bottom-right, 4000ms, 3 visible, rich colors, no close button, collapsed\n * stack that expands on hover, shadcn token CSS vars, Lucide icons. Toasts come from the\n * global `toast()` API; the region renders sonner's exact DOM (section > ol[data-sonner-toaster]\n * > li[data-sonner-toast]) and stylesheet, so stacking, swipe-to-dismiss and animations match.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-toaster, [ui-toaster]',\n  standalone: true,\n  encapsulation: ViewEncapsulation.None,\n  imports: [UiSonnerToastComponent],\n  host: {\n    class: 'contents',\n    '[attr.data-slot]': '\"sonner\"',\n    '[attr.data-uipkge]': '\"\"',\n  },\n  template: `\n    <section\n      [attr.aria-label]=\"containerAriaLabel + ' ' + hotkeyLabel\"\n      tabindex=\"-1\"\n      aria-live=\"polite\"\n      aria-relevant=\"additions text\"\n      aria-atomic=\"false\"\n    >\n      @for (pos of possiblePositions(); track pos; let i = $index) {\n        @if (filteredToasts().length) {\n          <ol\n            [attr.dir]=\"resolvedDir\"\n            tabindex=\"-1\"\n            [class]=\"listClass\"\n            data-sonner-toaster=\"true\"\n            [attr.data-sonner-theme]=\"actualTheme()\"\n            [attr.data-y-position]=\"pos.split('-')[0]\"\n            [attr.data-x-position]=\"pos.split('-')[1]\"\n            [style]=\"listStyle()\"\n            (focusout)=\"onListBlur($event)\"\n            (focusin)=\"onListFocus($event)\"\n            (mouseenter)=\"expanded.set(true)\"\n            (mousemove)=\"expanded.set(true)\"\n            (mouseleave)=\"onMouseLeave()\"\n            (dragend)=\"expanded.set(false)\"\n            (pointerdown)=\"onListPointerDown($event)\"\n            (pointerup)=\"interacting.set(false)\"\n          >\n            @for (t of toastsFor(pos, i); track t.id; let j = $index) {\n              <li ui-sonner-toast [toast]=\"t\" [index]=\"j\" [position]=\"pos\"></li>\n            }\n          </ol>\n        }\n      }\n    </section>\n  `,\n})\nexport class UiToasterComponent implements OnInit, OnDestroy {\n  private readonly el = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement\n  private readonly themeService = inject(ThemeService)\n\n  @Input() id?: string\n  @Input() invert?: boolean\n  /** Defaults to the app theme (ThemeService), like React's next-themes `useTheme()`. */\n  @Input()\n  set theme(value: ToasterTheme | undefined) {\n    this.themeInput.set(value)\n  }\n  get theme(): ToasterTheme | undefined {\n    return this.themeInput()\n  }\n  @Input() position: Position = 'bottom-right'\n  @Input() hotkey: string[] = ['altKey', 'KeyT']\n  @Input() richColors = true\n  @Input() expand = false\n  @Input() duration = 4000\n  @Input() gap = GAP\n  @Input() visibleToasts = VISIBLE_TOASTS_AMOUNT\n  @Input() closeButton = false\n  @Input() toastOptions?: ToastOptions\n  @Input('class') className?: string\n  @Input() style?: StyleMap\n  @Input() offset?: Offset\n  @Input() mobileOffset?: Offset\n  @Input() dir?: 'rtl' | 'ltr' | 'auto'\n  @Input() swipeDirections?: SwipeDirection[]\n  @Input() containerAriaLabel = 'Notifications'\n\n  readonly toasts = signal<ToastT[]>([])\n  readonly heights = signal<HeightT[]>([])\n  readonly expanded = signal(false)\n  readonly interacting = signal(false)\n  readonly documentHidden = signal(typeof document !== 'undefined' ? document.hidden : false)\n  private readonly systemDark = signal(\n    typeof window !== 'undefined' && !!window.matchMedia?.('(prefers-color-scheme: dark)').matches,\n  )\n  private unsubscribe?: () => void\n  private cleanups: (() => void)[] = []\n  private lastFocused: HTMLElement | null = null\n  private focusWithin = false\n\n  private readonly themeInput = signal<ToasterTheme | undefined>(undefined)\n  readonly actualTheme = computed(() => {\n    const theme = this.themeInput() ?? this.themeService.theme()\n    if (theme !== 'system') return theme\n    return this.systemDark() ? 'dark' : 'light'\n  })\n\n  readonly filteredToasts = computed(() =>\n    this.toasts().filter((t) => (this.id ? t.toasterId === this.id : !t.toasterId)),\n  )\n  readonly possiblePositions = computed(() =>\n    Array.from(\n      new Set([\n        this.position,\n        ...this.filteredToasts()\n          .filter((t) => t.position)\n          .map((t) => t.position!),\n      ]),\n    ),\n  )\n\n  get hotkeyLabel(): string {\n    return this.hotkey.join('+').replace(/Key/g, '').replace(/Digit/g, '')\n  }\n  get resolvedDir(): string {\n    const dir = this.dir ?? getDocumentDirection()\n    return dir === 'auto' ? getDocumentDirection() : dir\n  }\n  get listClass(): string {\n    return cn('toaster group', this.className)\n  }\n\n  listStyle(): StyleMap {\n    return {\n      '--front-toast-height': `${this.heights()[0]?.height || 0}px`,\n      '--width': `${TOAST_WIDTH}px`,\n      '--gap': `${this.gap}px`,\n      '--normal-bg': 'var(--popover)',\n      '--normal-text': 'var(--popover-foreground)',\n      '--normal-border': 'var(--border)',\n      '--border-radius': 'var(--radius)',\n      ...this.style,\n      ...assignOffset(this.offset, this.mobileOffset),\n    }\n  }\n\n  toastsFor(position: Position, index: number): ToastT[] {\n    return this.filteredToasts().filter((t) => (!t.position && index === 0) || t.position === position)\n  }\n\n  constructor() {\n    // sonner's stylesheet (~14 kB) goes into <head> once instead of `styles`: it's global\n    // either way (ViewEncapsulation.None), and Angular's default budget fails any\n    // component whose styles exceed 8 kB, which broke `ng build` for every app using this.\n    const doc = inject(DOCUMENT)\n    if (!doc.head.querySelector('style[data-uipkge-sonner]')) {\n      const style = doc.createElement('style')\n      style.setAttribute('data-uipkge-sonner', '')\n      style.textContent = SONNER_CSS\n      doc.head.appendChild(style)\n    }\n    // Ensure expanded is always false when no toasts are present / only one left.\n    effect(() => {\n      if (this.toasts().length <= 1) untracked(() => this.expanded.set(false))\n    })\n  }\n\n  ngOnInit(): void {\n    this.unsubscribe = ToastState.subscribe((t) => {\n      if ('dismiss' in t && (t as ToastToDismiss).dismiss) {\n        requestAnimationFrame(() =>\n          this.toasts.update((list) => list.map((x) => (x.id === t.id ? { ...x, delete: true } : x))),\n        )\n        return\n      }\n      setTimeout(() => {\n        this.toasts.update((list) => {\n          const i = list.findIndex((x) => x.id === t.id)\n          if (i !== -1) return [...list.slice(0, i), { ...list[i]!, ...(t as ToastT) }, ...list.slice(i + 1)]\n          return [t as ToastT, ...list]\n        })\n      })\n    })\n    if (typeof document === 'undefined') return\n    const onVisibility = () => this.documentHidden.set(document.hidden)\n    document.addEventListener('visibilitychange', onVisibility)\n    const onKeydown = (event: KeyboardEvent) => {\n      const list = this.el.querySelector<HTMLElement>('[data-sonner-toaster]')\n      const pressed = this.hotkey.every(\n        (key) => (event as unknown as Record<string, unknown>)[key] || event.code === key,\n      )\n      if (pressed) {\n        this.expanded.set(true)\n        list?.focus()\n      }\n      if (\n        event.code === 'Escape' &&\n        list &&\n        (document.activeElement === list || list.contains(document.activeElement))\n      ) {\n        this.expanded.set(false)\n      }\n    }\n    document.addEventListener('keydown', onKeydown)\n    const mql = window.matchMedia?.('(prefers-color-scheme: dark)')\n    const onScheme = (e: MediaQueryListEvent) => this.systemDark.set(e.matches)\n    mql?.addEventListener?.('change', onScheme)\n    this.cleanups.push(\n      () => document.removeEventListener('visibilitychange', onVisibility),\n      () => document.removeEventListener('keydown', onKeydown),\n      () => mql?.removeEventListener?.('change', onScheme),\n    )\n  }\n\n  removeToast(toRemove: ToastT): void {\n    this.toasts.update((list) => {\n      if (!list.find((t) => t.id === toRemove.id)?.delete) ToastState.dismiss(toRemove.id)\n      return list.filter(({ id }) => id !== toRemove.id)\n    })\n  }\n\n  onMouseLeave(): void {\n    if (!this.interacting()) this.expanded.set(false)\n  }\n\n  onListPointerDown(event: PointerEvent): void {\n    const notDismissible = event.target instanceof HTMLElement && event.target.dataset['dismissible'] === 'false'\n    if (!notDismissible) this.interacting.set(true)\n  }\n\n  onListFocus(event: FocusEvent): void {\n    const notDismissible = event.target instanceof HTMLElement && event.target.dataset['dismissible'] === 'false'\n    if (notDismissible) return\n    if (!this.focusWithin) {\n      this.focusWithin = true\n      this.lastFocused = event.relatedTarget as HTMLElement | null\n    }\n  }\n\n  onListBlur(event: FocusEvent): void {\n    const list = event.currentTarget as HTMLElement\n    if (this.focusWithin && !list.contains(event.relatedTarget as Node | null)) {\n      this.focusWithin = false\n      this.lastFocused?.focus({ preventScroll: true })\n      this.lastFocused = null\n    }\n  }\n\n  ngOnDestroy(): void {\n    this.unsubscribe?.()\n    this.cleanups.splice(0).forEach((fn) => fn())\n    this.lastFocused?.focus({ preventScroll: true })\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/sonner/sonner.component.ts"
    },
    {
      "path": "packages/registry-angular/components/sonner/index.ts",
      "content": "export {\n  UiToasterComponent,\n  toast,\n  ToastState,\n  type Action,\n  type ExternalToast,\n  type HeightT,\n  type Position,\n  type PromiseData,\n  type SwipeDirection,\n  type ToastClassnames,\n  type ToastOptions,\n  type ToastT,\n  type ToastTypes,\n  type ToasterTheme,\n} from './sonner.component'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/sonner/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/use-theme.json"
  ],
  "description": "Toast notification system — non-blocking, auto-dismissing alerts that stack in a corner. A dependency-free port of `sonner` (same `toast()` API, DOM and stylesheet) with the registry’s tokens applied.",
  "categories": [
    "feedback"
  ]
}