{
  "name": "toast",
  "dependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "toast.tsx",
      "content": "\"use client\";\r\n\r\nimport { css, fadeOut, slideInFromBottom, themeVars as theme } from \"@yugnex/core\";\r\nimport { usePortal } from \"@yugnex/core/client\";\r\nimport { useEffect, useRef, useState, useSyncExternalStore, type ReactNode } from \"react\";\r\nimport { createPortal } from \"react-dom\";\r\n\r\nexport interface ToastData {\r\n  id: string;\r\n  title?: ReactNode;\r\n  description?: ReactNode;\r\n  tone?: \"default\" | \"success\" | \"warning\" | \"destructive\";\r\n  duration?: number;\r\n}\r\n\r\ntype Listener = () => void;\r\n\r\nlet toasts: ToastData[] = [];\r\nconst listeners = new Set<Listener>();\r\n\r\nfunction emit(): void {\r\n  listeners.forEach((listener) => listener());\r\n}\r\n\r\nfunction subscribe(listener: Listener): () => void {\r\n  listeners.add(listener);\r\n  return () => listeners.delete(listener);\r\n}\r\n\r\nfunction getSnapshot(): ToastData[] {\r\n  return toasts;\r\n}\r\n\r\nfunction getServerSnapshot(): ToastData[] {\r\n  return [];\r\n}\r\n\r\nlet idCounter = 0;\r\n\r\n/** Queue a toast from anywhere in the app. Requires a mounted <ToastViewport />. */\r\nexport function toast(data: Omit<ToastData, \"id\">): string {\r\n  const id = `nx-toast-${++idCounter}`;\r\n  toasts = [...toasts, { duration: 4000, tone: \"default\", ...data, id }];\r\n  emit();\r\n  return id;\r\n}\r\n\r\nexport function dismissToast(id: string): void {\r\n  toasts = toasts.filter((item) => item.id !== id);\r\n  emit();\r\n}\r\n\r\nconst viewportClass = css({\r\n  position: \"fixed\",\r\n  bottom: theme.space[4],\r\n  right: theme.space[4],\r\n  zIndex: theme.zIndex.toast,\r\n  display: \"flex\",\r\n  flexDirection: \"column\",\r\n  gap: theme.space[2],\r\n  width: \"min(24rem, calc(100vw - 2rem))\",\r\n  pointerEvents: \"none\",\r\n});\r\n\r\nconst itemClass = css({\r\n  pointerEvents: \"auto\",\r\n  borderRadius: theme.radius.md,\r\n  border: `1px solid ${theme.color.border}`,\r\n  backgroundColor: theme.color.card,\r\n  color: theme.color.cardForeground,\r\n  boxShadow: theme.shadow.lg,\r\n  padding: theme.space[4],\r\n  fontFamily: theme.fontFamily.sans,\r\n  '&[data-state=\"open\"]': {\r\n    animation: `${slideInFromBottom} ${theme.duration.base} ${theme.easing.decelerate}`,\r\n  },\r\n  '&[data-state=\"closed\"]': {\r\n    animation: `${fadeOut} ${theme.duration.fast} ${theme.easing.accelerate}`,\r\n  },\r\n});\r\n\r\nconst titleClass = css({ fontSize: theme.fontSize.sm, fontWeight: theme.fontWeight.semibold, margin: 0 });\r\nconst descriptionClass = css({\r\n  marginTop: theme.space[1],\r\n  fontSize: theme.fontSize.sm,\r\n  color: theme.color.mutedForeground,\r\n});\r\n\r\nconst toneBorderColor: Record<NonNullable<ToastData[\"tone\"]>, string> = {\r\n  default: theme.color.border,\r\n  success: theme.color.success,\r\n  warning: theme.color.warning,\r\n  destructive: theme.color.destructive,\r\n};\r\n\r\nfunction ToastItem({ data }: { data: ToastData }) {\r\n  const [dataState, setDataState] = useState<\"open\" | \"closed\">(\"open\");\r\n  const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\r\n  const remainingRef = useRef(data.duration ?? 4000);\r\n  const startedAtRef = useRef(Date.now());\r\n\r\n  const close = () => {\r\n    setDataState(\"closed\");\r\n    setTimeout(() => dismissToast(data.id), 150);\r\n  };\r\n\r\n  const startTimer = (ms: number) => {\r\n    startedAtRef.current = Date.now();\r\n    timeoutRef.current = setTimeout(close, ms);\r\n  };\r\n\r\n  useEffect(() => {\r\n    startTimer(remainingRef.current);\r\n    return () => {\r\n      if (timeoutRef.current) clearTimeout(timeoutRef.current);\r\n    };\r\n  }, []);\r\n\r\n  const pause = () => {\r\n    if (timeoutRef.current) clearTimeout(timeoutRef.current);\r\n    remainingRef.current -= Date.now() - startedAtRef.current;\r\n  };\r\n\r\n  const resume = () => {\r\n    startTimer(Math.max(remainingRef.current, 0));\r\n  };\r\n\r\n  return (\r\n    <div\r\n      role=\"status\"\r\n      aria-live=\"polite\"\r\n      data-state={dataState}\r\n      className={itemClass}\r\n      style={{ borderColor: toneBorderColor[data.tone ?? \"default\"] }}\r\n      onMouseEnter={pause}\r\n      onMouseLeave={resume}\r\n    >\r\n      {data.title ? <p className={titleClass}>{data.title}</p> : null}\r\n      {data.description ? <p className={descriptionClass}>{data.description}</p> : null}\r\n    </div>\r\n  );\r\n}\r\n\r\n/** Mount once near the root. Every toast() call renders here via a portal. */\r\nexport function ToastViewport() {\r\n  const items = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\r\n  const portalNode = usePortal(\"nexui-toast-viewport\");\r\n\r\n  if (!portalNode) return null;\r\n\r\n  return createPortal(\r\n    <div className={viewportClass}>\r\n      {items.map((item) => (\r\n        <ToastItem key={item.id} data={item} />\r\n      ))}\r\n    </div>,\r\n    portalNode,\r\n  );\r\n}\r\n",
      "type": "registry:component"
    }
  ]
}