{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sonner",
  "type": "registry:ui",
  "title": "Sonner",
  "description": "Toast notifications for Tile UI.",
  "devDependencies": [
    "sass"
  ],
  "registryDependencies": [
    "@tile-ui/core",
    "@tile-ui/styles"
  ],
  "files": [
    {
      "path": "packages/react/src/components/sonner/sonner.tsx",
      "type": "registry:ui",
      "target": "components/ui/sonner/sonner.tsx",
      "content": "import React from 'react';\nimport { useSyncExternalStore } from 'react';\nimport { SONNER_DEFAULT_DURATION, buildSonnerToastApi, createSonnerStore, getSonnerPositionStyleKeys, resolveSonnerTheme, sonnerStyleKeys } from '../lib/core';\nimport type { SonnerPosition, SonnerStore, SonnerTheme, SonnerToast, SonnerToastApi, SonnerToasterBaseProps, SonnerType } from '../lib/core';\nimport styles from './sonner.module.scss';\n\nconst emptyToasts: SonnerToast[] = Object.freeze([]) as unknown as SonnerToast[];\nconst toasterOwners = new Map<symbol, number | undefined>();\nconst ownerListeners = new Set<() => void>();\nlet browserStore: SonnerStore | undefined;\nlet activeOwner: symbol | undefined;\n\nfunction getBrowserStore(): SonnerStore | undefined {\n\tif (typeof window === 'undefined') return undefined;\n\treturn (browserStore ??= createSonnerStore());\n}\n\nconst storeFacade: SonnerStore = {\n\tgetToasts: () => getBrowserStore()?.getToasts() ?? emptyToasts,\n\tsubscribe: (listener) => getBrowserStore()?.subscribe(listener) ?? (() => undefined),\n\tadd: (input) => getBrowserStore()?.add(input) ?? '',\n\tupdate: (id, patch) => getBrowserStore()?.update(id, patch),\n\tdismiss: (id) => getBrowserStore()?.dismiss(id),\n\tremove: (id) => getBrowserStore()?.remove(id),\n\tdismissAll: () => getBrowserStore()?.dismissAll(),\n\tsetDefaultDuration: (duration) => getBrowserStore()?.setDefaultDuration(duration),\n};\n\nfunction applyToasterPolicy() {\n\tconst owner = Array.from(toasterOwners.entries()).at(-1);\n\tactiveOwner = owner?.[0];\n\tgetBrowserStore()?.setDefaultDuration(owner?.[1] ?? SONNER_DEFAULT_DURATION);\n\tfor (const listener of ownerListeners) listener();\n}\n\nexport const toast: SonnerToastApi = buildSonnerToastApi(storeFacade);\n\nexport interface UseToastReturn {\n\ttoasts: SonnerToast[];\n\ttoast: SonnerToastApi;\n\tdismiss: (id?: string) => void;\n\tdismissAll: () => void;\n}\n\nfunction useToast(): UseToastReturn {\n\tconst toasts = useSyncExternalStore(storeFacade.subscribe, storeFacade.getToasts, () => emptyToasts);\n\treturn {\n\t\ttoasts,\n\t\ttoast,\n\t\tdismiss: (id) => toast.dismiss(id),\n\t\tdismissAll: () => toast.dismissAll(),\n\t};\n}\n\nfunction SuccessIcon() {\n\treturn (\n\t\t<svg\n\t\t\txmlns=\"http://www.w3.org/2000/svg\"\n\t\t\twidth=\"16\"\n\t\t\theight=\"16\"\n\t\t\tviewBox=\"0 0 24 24\"\n\t\t\tfill=\"none\"\n\t\t\tstroke=\"currentColor\"\n\t\t\tstrokeWidth=\"2\"\n\t\t\tstrokeLinecap=\"round\"\n\t\t\tstrokeLinejoin=\"round\"\n\t\t\taria-hidden=\"true\">\n\t\t\t<circle cx=\"12\" cy=\"12\" r=\"10\" />\n\t\t\t<path d=\"m9 12 2 2 4-4\" />\n\t\t</svg>\n\t);\n}\n\nfunction InfoIcon() {\n\treturn (\n\t\t<svg\n\t\t\txmlns=\"http://www.w3.org/2000/svg\"\n\t\t\twidth=\"16\"\n\t\t\theight=\"16\"\n\t\t\tviewBox=\"0 0 24 24\"\n\t\t\tfill=\"none\"\n\t\t\tstroke=\"currentColor\"\n\t\t\tstrokeWidth=\"2\"\n\t\t\tstrokeLinecap=\"round\"\n\t\t\tstrokeLinejoin=\"round\"\n\t\t\taria-hidden=\"true\">\n\t\t\t<circle cx=\"12\" cy=\"12\" r=\"10\" />\n\t\t\t<path d=\"M12 16v-4\" />\n\t\t\t<path d=\"M12 8h.01\" />\n\t\t</svg>\n\t);\n}\n\nfunction WarningIcon() {\n\treturn (\n\t\t<svg\n\t\t\txmlns=\"http://www.w3.org/2000/svg\"\n\t\t\twidth=\"16\"\n\t\t\theight=\"16\"\n\t\t\tviewBox=\"0 0 24 24\"\n\t\t\tfill=\"none\"\n\t\t\tstroke=\"currentColor\"\n\t\t\tstrokeWidth=\"2\"\n\t\t\tstrokeLinecap=\"round\"\n\t\t\tstrokeLinejoin=\"round\"\n\t\t\taria-hidden=\"true\">\n\t\t\t<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-3Z\" />\n\t\t\t<path d=\"M12 9v4\" />\n\t\t\t<path d=\"M12 17h.01\" />\n\t\t</svg>\n\t);\n}\n\nfunction ErrorIcon() {\n\treturn (\n\t\t<svg\n\t\t\txmlns=\"http://www.w3.org/2000/svg\"\n\t\t\twidth=\"16\"\n\t\t\theight=\"16\"\n\t\t\tviewBox=\"0 0 24 24\"\n\t\t\tfill=\"none\"\n\t\t\tstroke=\"currentColor\"\n\t\t\tstrokeWidth=\"2\"\n\t\t\tstrokeLinecap=\"round\"\n\t\t\tstrokeLinejoin=\"round\"\n\t\t\taria-hidden=\"true\">\n\t\t\t<path d=\"M13 2 3 14h9l-1 8 10-12h-9l1-8Z\" />\n\t\t</svg>\n\t);\n}\n\nfunction LoadingIcon() {\n\treturn (\n\t\t<svg\n\t\t\txmlns=\"http://www.w3.org/2000/svg\"\n\t\t\twidth=\"16\"\n\t\t\theight=\"16\"\n\t\t\tviewBox=\"0 0 24 24\"\n\t\t\tfill=\"none\"\n\t\t\tstroke=\"currentColor\"\n\t\t\tstrokeWidth=\"2\"\n\t\t\tstrokeLinecap=\"round\"\n\t\t\tstrokeLinejoin=\"round\"\n\t\t\taria-hidden=\"true\">\n\t\t\t<path d=\"M21 12a9 9 0 1 1-6.219-8.56\" />\n\t\t</svg>\n\t);\n}\n\nfunction CloseIcon() {\n\treturn (\n\t\t<svg\n\t\t\txmlns=\"http://www.w3.org/2000/svg\"\n\t\t\twidth=\"16\"\n\t\t\theight=\"16\"\n\t\t\tviewBox=\"0 0 24 24\"\n\t\t\tfill=\"none\"\n\t\t\tstroke=\"currentColor\"\n\t\t\tstrokeWidth=\"2\"\n\t\t\tstrokeLinecap=\"round\"\n\t\t\tstrokeLinejoin=\"round\"\n\t\t\taria-hidden=\"true\">\n\t\t\t<path d=\"M18 6 6 18\" />\n\t\t\t<path d=\"m6 6 12 12\" />\n\t\t</svg>\n\t);\n}\n\nfunction ToastIcon({ type }: { type: SonnerType }) {\n\tswitch (type) {\n\t\tcase 'success':\n\t\t\treturn <SuccessIcon />;\n\t\tcase 'info':\n\t\t\treturn <InfoIcon />;\n\t\tcase 'warning':\n\t\t\treturn <WarningIcon />;\n\t\tcase 'error':\n\t\t\treturn <ErrorIcon />;\n\t\tcase 'loading':\n\t\t\treturn <LoadingIcon />;\n\t\tdefault:\n\t\t\treturn null;\n\t}\n}\n\nfunction toastRole(type: SonnerType): 'alert' | 'status' {\n\treturn type === 'error' || type === 'warning' ? 'alert' : 'status';\n}\n\nfunction useResolvedTheme(theme: SonnerTheme | undefined): 'light' | 'dark' | undefined {\n\tconst [systemTheme, setSystemTheme] = React.useState<'light' | 'dark'>('light');\n\n\tReact.useEffect(() => {\n\t\tif (theme !== 'system') {\n\t\t\treturn;\n\t\t}\n\t\tconst media = window.matchMedia('(prefers-color-scheme: dark)');\n\t\tconst update = () => setSystemTheme(media.matches ? 'dark' : 'light');\n\t\tupdate();\n\t\tmedia.addEventListener('change', update);\n\t\treturn () => media.removeEventListener('change', update);\n\t}, [theme]);\n\n\treturn resolveSonnerTheme(theme, systemTheme === 'dark');\n}\n\nexport interface ToasterProps extends SonnerToasterBaseProps {\n\tclassName?: string;\n}\n\nfunction Toaster({ position = 'bottom-right', duration, theme, richColors = false, closeButton = true, className = '' }: ToasterProps) {\n\tconst owner = React.useRef(Symbol('sonner-toaster-owner')).current;\n\tconst toasts = useSyncExternalStore(storeFacade.subscribe, storeFacade.getToasts, () => emptyToasts);\n\tconst currentOwner = useSyncExternalStore(\n\t\t(listener) => {\n\t\t\townerListeners.add(listener);\n\t\t\treturn () => ownerListeners.delete(listener);\n\t\t},\n\t\t() => activeOwner,\n\t\t() => undefined,\n\t);\n\tconst resolvedTheme = useResolvedTheme(theme);\n\n\tReact.useEffect(() => {\n\t\ttoasterOwners.set(owner, duration);\n\t\tapplyToasterPolicy();\n\t\treturn () => {\n\t\t\ttoasterOwners.delete(owner);\n\t\t\tapplyToasterPolicy();\n\t\t};\n\t}, [duration, owner]);\n\n\tReact.useEffect(() => {\n\t\tif (!toasterOwners.has(owner)) return;\n\t\ttoasterOwners.set(owner, duration);\n\t\tapplyToasterPolicy();\n\t}, [duration, owner]);\n\n\tconst groups = new Map<string, SonnerToast[]>();\n\tfor (const item of currentOwner === owner ? toasts : emptyToasts) {\n\t\tconst key = item.position ?? position;\n\t\tconst list = groups.get(key);\n\t\tif (list) {\n\t\t\tlist.push(item);\n\t\t} else {\n\t\t\tgroups.set(key, [item]);\n\t\t}\n\t}\n\n\treturn (\n\t\t<>\n\t\t\t{Array.from(groups.entries()).map(([groupPosition, list]) => {\n\t\t\t\tconst styleKeys = getSonnerPositionStyleKeys(groupPosition as SonnerPosition);\n\t\t\t\treturn (\n\t\t\t\t\t<div\n\t\t\t\t\t\tkey={groupPosition}\n\t\t\t\t\t\tdata-slot=\"toaster\"\n\t\t\t\t\t\tdata-theme={resolvedTheme}\n\t\t\t\t\t\tdata-rich-colors={richColors ? 'true' : undefined}\n\t\t\t\t\t\tclassName={[styles[styleKeys.base], styles[styleKeys.position], resolvedTheme, className].filter(Boolean).join(' ')}>\n\t\t\t\t\t\t{list.map((item) => (\n\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\tkey={item.id}\n\t\t\t\t\t\t\t\trole={toastRole(item.type)}\n\t\t\t\t\t\t\t\taria-live={toastRole(item.type) === 'alert' ? 'assertive' : 'polite'}\n\t\t\t\t\t\t\t\taria-atomic=\"true\"\n\t\t\t\t\t\t\t\tdata-slot=\"toast\"\n\t\t\t\t\t\t\t\tdata-type={item.type}\n\t\t\t\t\t\t\t\tdata-dismissing={item.dismissing}\n\t\t\t\t\t\t\t\tdata-rich-colors={item.richColors === undefined ? undefined : item.richColors ? 'true' : 'false'}\n\t\t\t\t\t\t\t\tclassName={styles[sonnerStyleKeys.toast]}>\n\t\t\t\t\t\t\t\t{item.type !== 'default' && (\n\t\t\t\t\t\t\t\t\t<div className={styles[sonnerStyleKeys.icon]}>\n\t\t\t\t\t\t\t\t\t\t<ToastIcon type={item.type} />\n\t\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t<div className={styles[sonnerStyleKeys.content]}>\n\t\t\t\t\t\t\t\t\t{item.title !== undefined && <div className={styles[sonnerStyleKeys.title]}>{item.title}</div>}\n\t\t\t\t\t\t\t\t\t{item.description !== undefined && <div className={styles[sonnerStyleKeys.description]}>{item.description}</div>}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t\t{closeButton && item.dismissible !== false && (\n\t\t\t\t\t\t\t\t\t<button type=\"button\" aria-label=\"关闭\" className={styles[sonnerStyleKeys.close]} onClick={() => toast.dismiss(item.id)}>\n\t\t\t\t\t\t\t\t\t\t<CloseIcon />\n\t\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t))}\n\t\t\t\t\t</div>\n\t\t\t\t);\n\t\t\t})}\n\t\t</>\n\t);\n}\n\nexport { Toaster, useToast };\nexport default Toaster;\n"
    },
    {
      "path": "packages/styles/scss/components/sonner.module.scss",
      "type": "registry:file",
      "target": "components/ui/sonner/sonner.module.scss",
      "content": "@use '../../../styles/variables/colors' as *;\n\n// Toaster 容器\n.root {\n\tposition: fixed;\n\tz-index: $z-modal;\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: $spacing-2;\n\twidth: max-content;\n\tmax-width: calc(100vw - 2rem);\n\tpointer-events: none;\n\n\t&.positionTopLeft {\n\t\ttop: $spacing-4;\n\t\tleft: $spacing-4;\n\t}\n\n\t&.positionTopRight {\n\t\ttop: $spacing-4;\n\t\tright: $spacing-4;\n\t}\n\n\t&.positionTopCenter {\n\t\ttop: $spacing-4;\n\t\tleft: 50%;\n\t\ttransform: translateX(-50%);\n\t}\n\n\t&.positionBottomLeft {\n\t\tbottom: $spacing-4;\n\t\tleft: $spacing-4;\n\t}\n\n\t&.positionBottomRight {\n\t\tright: $spacing-4;\n\t\tbottom: $spacing-4;\n\t}\n\n\t&.positionBottomCenter {\n\t\tbottom: $spacing-4;\n\t\tleft: 50%;\n\t\ttransform: translateX(-50%);\n\t}\n}\n\n// 单条提示\n.toast {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: $spacing-3;\n\tmin-width: 0;\n\tborder: 1px solid var(--tile-toast-border, $border);\n\tborder-radius: var(--tile-toast-radius, $radius);\n\tbackground-color: var(--tile-toast-background, $popover);\n\tcolor: $popover-foreground;\n\tpadding: var(--tile-toast-padding, $spacing-3 $spacing-4);\n\tbox-shadow: var(--tile-toast-shadow, $shadow-md);\n\toutline: var(--tile-toast-outline, none);\n\tpointer-events: auto;\n\ttransition:\n\t\topacity $transition-normal,\n\t\ttransform $transition-normal;\n\n\t&[data-dismissing='true'] {\n\t\topacity: 0;\n\t\ttransform: translateY(0.5rem);\n\t}\n\n\t// Optional status surfaces and rings can style titles independently.\n\t// Unset tokens leave Tile's regular (non-rich) toast untouched.\n\t@each $type in success, info, warning, error {\n\t\t&[data-type='#{$type}'] {\n\t\t\tbackground-color: var(--tile-toast-#{$type}-background, var(--tile-toast-background, $popover));\n\t\t\toutline: var(--tile-toast-#{$type}-outline, var(--tile-toast-outline, none));\n\n\t\t\t.title {\n\t\t\t\tcolor: var(--tile-toast-#{$type}-title-color, inherit);\n\t\t\t}\n\t\t}\n\t}\n}\n\n// 图标\n.icon {\n\tdisplay: flex;\n\tflex-shrink: 0;\n\talign-items: center;\n\tjustify-content: center;\n\twidth: $spacing-5;\n\theight: $spacing-5;\n\n\tsvg {\n\t\twidth: $spacing-4;\n\t\theight: $spacing-4;\n\t}\n\n\t.toast[data-type='success'] & {\n\t\tcolor: var(--tile-toast-success-color, $success);\n\t}\n\n\t.toast[data-type='info'] & {\n\t\tcolor: var(--tile-toast-info-color, $info);\n\t}\n\n\t.toast[data-type='warning'] & {\n\t\tcolor: var(--tile-toast-warning-color, $warning);\n\t}\n\n\t.toast[data-type='error'] & {\n\t\tcolor: var(--tile-toast-error-color, $destructive);\n\t}\n}\n\n// 内容\n.content {\n\tdisplay: flex;\n\tmin-width: 0;\n\tflex: 1;\n\tflex-direction: column;\n}\n\n// 标题\n.title {\n\tfont-size: var(--tile-toast-title-size, $text-sm);\n\tfont-weight: $font-medium;\n\tline-height: var(--tile-toast-title-leading, $leading-tight);\n}\n\n// 描述\n.description {\n\tmargin-top: 0.125rem;\n\tfont-size: var(--tile-toast-description-size, $text-sm);\n\tcolor: var(--tile-toast-description-color, $muted-foreground);\n\tline-height: var(--tile-toast-description-leading, $leading-normal);\n}\n\n// 富色模式：按类型着色整条提示\n.root[data-rich-colors='true'] {\n\t.toast:not([data-rich-colors='false']) {\n\t\tborder-color: transparent;\n\n\t\t&[data-type='success'] {\n\t\t\tbackground-color: var(--tile-toast-success-background, $success);\n\t\t\tcolor: var(--tile-toast-rich-color, $success-foreground);\n\n\t\t\t.icon {\n\t\t\t\tcolor: var(--tile-toast-success-color, $success-foreground);\n\t\t\t}\n\t\t}\n\n\t\t&[data-type='info'] {\n\t\t\tbackground-color: var(--tile-toast-info-background, $info);\n\t\t\tcolor: var(--tile-toast-rich-color, $info-foreground);\n\n\t\t\t.icon {\n\t\t\t\tcolor: var(--tile-toast-info-color, $info-foreground);\n\t\t\t}\n\t\t}\n\n\t\t&[data-type='warning'] {\n\t\t\tbackground-color: var(--tile-toast-warning-background, $warning);\n\t\t\tcolor: var(--tile-toast-rich-color, $warning-foreground);\n\n\t\t\t.icon {\n\t\t\t\tcolor: var(--tile-toast-warning-color, $warning-foreground);\n\t\t\t}\n\t\t}\n\n\t\t&[data-type='error'] {\n\t\t\tbackground-color: var(--tile-toast-error-background, $destructive);\n\t\t\tcolor: var(--tile-toast-rich-color, $destructive-foreground);\n\n\t\t\t.icon {\n\t\t\t\tcolor: var(--tile-toast-error-color, $destructive-foreground);\n\t\t\t}\n\t\t}\n\t}\n}\n\n.toast[data-rich-colors='true'] {\n\tborder-color: transparent;\n\n\t&[data-type='success'] {\n\t\tbackground-color: var(--tile-toast-success-background, $success);\n\t\tcolor: var(--tile-toast-rich-color, $success-foreground);\n\n\t\t.icon {\n\t\t\tcolor: var(--tile-toast-success-color, $success-foreground);\n\t\t}\n\t}\n\n\t&[data-type='info'] {\n\t\tbackground-color: var(--tile-toast-info-background, $info);\n\t\tcolor: var(--tile-toast-rich-color, $info-foreground);\n\n\t\t.icon {\n\t\t\tcolor: var(--tile-toast-info-color, $info-foreground);\n\t\t}\n\t}\n\n\t&[data-type='warning'] {\n\t\tbackground-color: var(--tile-toast-warning-background, $warning);\n\t\tcolor: var(--tile-toast-rich-color, $warning-foreground);\n\n\t\t.icon {\n\t\t\tcolor: var(--tile-toast-warning-color, $warning-foreground);\n\t\t}\n\t}\n\n\t&[data-type='error'] {\n\t\tbackground-color: var(--tile-toast-error-background, $destructive);\n\t\tcolor: var(--tile-toast-rich-color, $destructive-foreground);\n\n\t\t.icon {\n\t\t\tcolor: var(--tile-toast-error-color, $destructive-foreground);\n\t\t}\n\t}\n}\n\n.root[data-rich-colors='true'] .toast:not([data-rich-colors='false']),\n.toast[data-rich-colors='true'] {\n\t.description,\n\t.close,\n\t.close:hover {\n\t\tcolor: var(--tile-toast-rich-description-color, inherit);\n\t}\n}\n\n// 关闭按钮\n.close {\n\tdisplay: inline-flex;\n\tflex-shrink: 0;\n\talign-items: center;\n\tjustify-content: center;\n\tborder: none;\n\tborder-radius: var(--tile-toast-close-radius, $radius);\n\tbackground: transparent;\n\tcolor: $muted-foreground;\n\tcursor: pointer;\n\tpadding: $spacing-1;\n\ttransition:\n\t\tbackground-color $transition-fast,\n\t\tcolor $transition-fast;\n\n\t&:hover {\n\t\tbackground-color: var(--tile-toast-close-hover-background, $muted);\n\t\tcolor: $foreground;\n\t}\n\n\tsvg {\n\t\twidth: $spacing-4;\n\t\theight: $spacing-4;\n\t}\n}\n\n// 操作按钮容器\n.actions {\n\tdisplay: flex;\n\tflex-shrink: 0;\n\talign-items: center;\n\tgap: $spacing-1;\n}\n"
    },
    {
      "path": "packages/react/src/components/sonner/index.ts",
      "type": "registry:ui",
      "target": "components/ui/sonner/index.ts",
      "content": "export { Toaster, useToast, toast, type ToasterProps, type UseToastReturn } from './sonner';\nexport type { SonnerAddInput, SonnerPosition, SonnerStore, SonnerTheme, SonnerToast, SonnerToastApi, SonnerToasterBaseProps, SonnerToastUpdate, SonnerType } from '../lib/core';\n"
    }
  ]
}
