{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "toast",
  "title": "Toast",
  "description": "A toast component.",
  "dependencies": [
    "class-variance-authority",
    "motion",
    "@hugeicons/react",
    "@hugeicons/core-free-icons"
  ],
  "registryDependencies": [
    "@cubby-ui/button",
    "@cubby-ui/elevated"
  ],
  "files": [
    {
      "path": "registry/default/toast/toast.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cva } from \"class-variance-authority\";\nimport { Popover } from \"@base-ui/react/popover\";\nimport { Toast } from \"@base-ui/react/toast\";\nimport { AnimatePresence } from \"motion/react\";\nimport { motion } from \"motion/react\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport {\n  AlertCircleIcon,\n  Cancel01Icon,\n  CheckmarkCircle02Icon,\n  InformationCircleIcon,\n  Loading03Icon,\n  TriangleAlertIcon,\n} from \"@hugeicons/core-free-icons\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/registry/default/button/button\";\nimport { solidSurface } from \"@/registry/default/lib/elevated\";\nimport \"./toast.css\";\n\nconst toastManager = Toast.createToastManager();\nconst anchoredToastManager = Toast.createToastManager();\n\n// Map groupId -> Base UI toastId\nconst groupToToastMap = new Map<string, string>();\n// Map itemId -> groupId for lookups\nconst groupItemToGroupMap = new Map<string, string>();\n// Map groupId -> GroupedToastData (toastManager doesn't expose getSnapshot)\nconst groupDataMap = new Map<string, GroupedToastData>();\n\nconst TOAST_ICONS = {\n  success: CheckmarkCircle02Icon,\n  error: AlertCircleIcon,\n  warning: TriangleAlertIcon,\n  info: InformationCircleIcon,\n  loading: Loading03Icon,\n} as const;\n\nconst toastIconVariants = cva(\"\", {\n  variants: {\n    type: {\n      default: \"\",\n      success: \"text-success-foreground\",\n      error: \"text-danger-foreground\",\n      warning: \"text-warning-foreground\",\n      info: \"text-info-foreground\",\n      loading: \"text-muted-foreground animate-spin\",\n    },\n  },\n});\n\ntype SwipeDirection = \"up\" | \"down\" | \"left\" | \"right\";\n\nfunction getSwipeDirection(position: ToastPosition): SwipeDirection[] {\n  const vertical: SwipeDirection = position.startsWith(\"top\") ? \"up\" : \"down\";\n  if (position.includes(\"center\")) return [vertical];\n  if (position.includes(\"left\")) return [\"left\", vertical];\n  return [\"right\", vertical];\n}\n\nfunction upsertReplayClassName(toast: {\n  updateKey?: number;\n}): string | undefined {\n  if (!toast.updateKey) return undefined;\n  return toast.updateKey % 2 === 0\n    ? \"animate-[pulse-even_0.28s_ease]\"\n    : \"animate-[pulse-odd_0.28s_ease]\";\n}\n\n// Shared classes for stacked toast roots (StackedToastItem + GroupedToastRoot).\n// Each callsite appends its own extras (pulse animation, overflow behavior, etc).\n// Shadow level=4 (heavier than a popover — toast floats unattached from any anchor);\n// the bg uses color-mix so stacked toasts get progressively darker.\nconst TOAST_ROOT_CLASSES = [\n  // Base styles & visual\n  \"text-popover-foreground data-expanded:bg-(--popup-surface) absolute z-[calc(9999-var(--toast-index))] h-(--toast-calc-height) w-full rounded-lg select-none [transition:transform_.5s_cubic-bezier(.22,1,.36,1),opacity_.5s,height_.15s,background-color_.5s]\",\n  // Surface elevation — manual composition because the bg uses color-mix for stacking.\n  // `[--popup-surface:var(--surface-3)]` exposes the surface color to descendants;\n  // `shadow-[drops-4,rim-3]` pairs a level-4 shadow weight with a level-3 rim color.\n  \"[--popup-surface:var(--surface-3)]\",\n  \"shadow-[var(--surface-shadow-4),var(--surface-rim-3)]\",\n  // Stacked-depth bg: tints --popup-surface 1% darker per toast-index layer\n  \"bg-[color-mix(in_srgb,var(--popup-surface),var(--color-black)_calc(1%*max(0,var(--toast-index,0))))]\",\n  // Positioning\n  \"data-[position*=top]:top-0 data-[position*=top]:right-0 data-[position*=top]:left-0 data-[position*=top]:origin-[50%_calc(50%-50%*min(var(--toast-index,0),1))]\",\n  \"data-[position*=bottom]:right-0 data-[position*=bottom]:bottom-0 data-[position*=bottom]:left-0 data-[position*=bottom]:origin-[50%_calc(50%+50%*min(var(--toast-index,0),1))]\",\n  // Gap fill for hover\n  \"after:absolute after:left-0 after:h-[calc(var(--toast-gap)+1px)] after:w-full\",\n  \"data-[position*=top]:after:bottom-full\",\n  \"data-[position*=bottom]:after:top-full\",\n  // CSS variables\n  \"[--toast-calc-height:var(--toast-frontmost-height,var(--toast-height))] [--toast-gap:--spacing(3)] [--toast-peek:--spacing(3)] [--toast-scale:calc(max(0,1-(var(--toast-index)*0.1)))] [--toast-shrink:calc(1-var(--toast-scale))]\",\n  \"data-[position*=top]:[--toast-calc-offset-y:calc(var(--toast-offset-y)+(var(--toast-index)*var(--toast-gap))+var(--toast-swipe-movement-y))]\",\n  \"data-[position*=bottom]:[--toast-calc-offset-y:calc(var(--toast-offset-y)*-1+(var(--toast-index)*var(--toast-gap)*-1)+var(--toast-swipe-movement-y))]\",\n  // Default state transform\n  \"data-[position*=top]:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)+(var(--toast-index)*var(--toast-peek))+(var(--toast-shrink)*var(--toast-calc-height))))_scale(var(--toast-scale))]\",\n  \"data-[position*=bottom]:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--toast-peek))-(var(--toast-shrink)*var(--toast-calc-height))))_scale(var(--toast-scale))]\",\n  // Limited state\n  \"data-limited:opacity-0\",\n  // Expanded state\n  \"data-expanded:h-(--toast-height)\",\n  \"data-position:data-expanded:transform-[translateX(var(--toast-swipe-movement-x))_translateY(var(--toast-calc-offset-y))]\",\n  // Starting style\n  \"data-[position*=top]:data-starting-style:transform-[translateY(calc(-100%-var(--toast-inset)))]\",\n  \"data-[position*=bottom]:data-starting-style:transform-[translateY(calc(100%+var(--toast-inset)))]\",\n  // Ending style\n  \"data-ending-style:opacity-0\",\n  \"data-[position*=top]:data-ending-style:not-data-limited:not-data-swipe-direction:transform-[translateY(calc(-100%-var(--toast-inset)))]\",\n  \"data-[position*=bottom]:data-ending-style:not-data-limited:not-data-swipe-direction:transform-[translateY(calc(100%+var(--toast-inset)))]\",\n  // Swipe direction ending styles\n  \"data-ending-style:data-[swipe-direction=down]:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)+100%+var(--toast-inset)))]\",\n  \"data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(var(--toast-swipe-movement-x)-100%-var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]\",\n  \"data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(var(--toast-swipe-movement-x)+100%+var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]\",\n  \"data-ending-style:data-[swipe-direction=up]:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-100%-var(--toast-inset)))]\",\n  // Expanded swipe direction ending styles\n  \"data-expanded:data-ending-style:data-[swipe-direction=down]:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)+100%+var(--toast-inset)))]\",\n  \"data-expanded:data-ending-style:data-[swipe-direction=left]:transform-[translateX(calc(var(--toast-swipe-movement-x)-100%-var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]\",\n  \"data-expanded:data-ending-style:data-[swipe-direction=right]:transform-[translateX(calc(var(--toast-swipe-movement-x)+100%+var(--toast-inset)))_translateY(var(--toast-calc-offset-y))]\",\n  \"data-expanded:data-ending-style:data-[swipe-direction=up]:transform-[translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-100%-var(--toast-inset)))]\",\n];\n\nexport interface ToastOptions<TData extends object = object> {\n  /** Fixed ID for deduplication. If a toast with this ID already exists, it updates in place. */\n  id?: string;\n  title?: string;\n  description?: string;\n  type?: \"default\" | \"success\" | \"error\" | \"warning\" | \"info\";\n  duration?: number;\n  priority?: \"low\" | \"high\";\n  action?: {\n    label: string;\n    onClick: () => void;\n  };\n  data?: TData;\n  onClose?: () => void;\n  onRemove?: () => void;\n  /** Whether to show the close button. Defaults to true. */\n  showCloseButton?: boolean;\n}\n\nexport interface AnchoredToastOptions<\n  TData extends object = object,\n> extends Omit<ToastOptions<TData>, \"type\"> {\n  anchor: Element | React.RefObject<Element | null>;\n  side?: \"top\" | \"bottom\" | \"left\" | \"right\";\n  sideOffset?: number;\n  align?: \"start\" | \"center\" | \"end\";\n  alignOffset?: number;\n  arrow?: boolean;\n}\n\ninterface ToastData {\n  id: string;\n  updateKey?: number;\n  title?: string;\n  description?: string;\n  type?: \"default\" | \"success\" | \"error\" | \"warning\" | \"info\" | \"loading\";\n  action?: {\n    label: string;\n    onClick: () => void;\n  };\n  showCloseButton?: boolean;\n  data?: unknown;\n  positionerProps?: {\n    anchor?: Element | null;\n    side?: \"top\" | \"bottom\" | \"left\" | \"right\";\n    sideOffset?: number;\n    align?: \"start\" | \"center\" | \"end\";\n    alignOffset?: number;\n  };\n}\n\ntype ToastType =\n  | \"default\"\n  | \"loading\"\n  | \"success\"\n  | \"error\"\n  | \"warning\"\n  | \"info\";\n\n/** Duration in ms before a completed group item auto-dismisses */\nconst GROUP_ITEM_DISMISS_DURATION = 5000;\n\n/** Individual item within a grouped toast */\nexport interface GroupedToastItem {\n  id: string;\n  title?: string;\n  description?: string;\n  type?: ToastType;\n  action?: {\n    label: string;\n    onClick: () => void;\n  };\n  data?: object;\n  createdAt: number;\n  showCloseButton?: boolean;\n  /** Timestamp when this item transitioned to a completed state */\n  completedAt?: number;\n  /** Duration for progress bar after completing (loading items only). Defaults to GROUP_ITEM_DISMISS_DURATION. */\n  duration?: number;\n}\n\n/**\n * Counts passed to groupSummary function.\n *\n * Note: `completedCount`, `successCount`, `errorCount`, `warningCount`, and `infoCount`\n * are **historical** - they persist even after completed items are auto-dismissed.\n * This ensures accurate summaries like \"3 of 5 succeeded\" remain correct after items fade out.\n * Only `loadingCount` reflects currently visible loading items.\n */\nexport interface GroupSummaryCounts {\n  /** Number of items currently in loading state (reflects visible items) */\n  loadingCount: number;\n  /** Total items that have completed historically (persists after dismiss) */\n  completedCount: number;\n  /** Total items: loadingCount + completedCount */\n  totalCount: number;\n  /** Items that completed with type 'success' (historical, persists after dismiss) */\n  successCount: number;\n  /** Items that completed with type 'error' (historical, persists after dismiss) */\n  errorCount: number;\n  /** Items that completed with type 'warning' (historical, persists after dismiss) */\n  warningCount: number;\n  /** Items that completed with type 'info' (historical, persists after dismiss) */\n  infoCount: number;\n}\n\nexport interface GroupedToastOptions<TData extends object = object> {\n  groupId: string;\n  title?: string;\n  description?: string;\n  type?: ToastType;\n  action?: {\n    label: string;\n    onClick: () => void;\n  };\n  data?: TData;\n  onClose?: () => void;\n  onRemove?: () => void;\n  /** Whether to show the close button. Defaults to true. */\n  showCloseButton?: boolean;\n  /** Summary text or function. Function receives counts: loadingCount (in-progress items) and totalCount (all items including completed). */\n  groupSummary: string | ((counts: GroupSummaryCounts) => string);\n  groupAction?: {\n    label: string;\n    expandedLabel?: string;\n  };\n  /** Duration in ms before the group auto-dismisses. Resets on each new item added. */\n  duration?: number;\n}\n\ninterface GroupedPromiseMessage {\n  title?: string;\n  description?: string;\n  type?: ToastType;\n}\n\n/** Messages for each state of a grouped promise toast */\nexport interface GroupedPromiseMessages<T> {\n  loading: GroupedPromiseMessage & {\n    action?: { label: string; onClick: () => void };\n  };\n  success: PromiseMessage | ((data: T) => GroupedPromiseMessage);\n  error: PromiseMessage | ((error: Error) => GroupedPromiseMessage);\n  /** Message to show when aborted via AbortController. Defaults to error message. */\n  aborted?: PromiseMessage | (() => GroupedPromiseMessage);\n}\n\nexport interface GroupedPromiseOptions extends Omit<\n  GroupedToastOptions,\n  \"type\" | \"title\" | \"description\" | \"action\"\n> {\n  /** AbortSignal for cancellation support */\n  signal?: AbortSignal;\n}\n\n/** Historical counts that only increment (never decrement on item dismiss) */\ninterface HistoricalCounts {\n  success: number;\n  error: number;\n  warning: number;\n  info: number;\n}\n\ninterface GroupedToastData {\n  isGrouped: true;\n  groupId: string;\n  /** Pending items (loading state) */\n  items: GroupedToastItem[];\n  /** Completed items that are transitioning out with progress bar */\n  completedItems: GroupedToastItem[];\n  isExpanded: boolean;\n  summary: string | ((counts: GroupSummaryCounts) => string);\n  action: {\n    label: string;\n    expandedLabel: string;\n  };\n  /** True once we've shown \"All complete\" (2+ items completed simultaneously) - stays in summary mode */\n  hasShownAllComplete: boolean;\n  /** Duration in ms, stored so we can pass it on update to reset timer */\n  duration?: number;\n  /** Historical counts that persist even after items are dismissed */\n  historicalCounts: HistoricalCounts;\n}\n\nfunction baseToast(jsx: React.ReactElement): string | undefined;\nfunction baseToast<TData extends object = object>(\n  jsx: React.ReactElement,\n  options: Omit<ToastOptions<TData>, \"title\" | \"description\">,\n): string | undefined;\nfunction baseToast<TData extends object = object>(\n  options: ToastOptions<TData>,\n): string | undefined;\n\nfunction baseToast<TData extends object = object>(\n  optionsOrJSX: ToastOptions<TData> | React.ReactElement,\n  jsxOptions?: Omit<ToastOptions<TData>, \"title\" | \"description\">,\n): string | undefined {\n  if (React.isValidElement(optionsOrJSX)) {\n    return toastManager.add({\n      ...(jsxOptions?.id && { id: jsxOptions.id }),\n      title: \"\",\n      description: \"\",\n      type: jsxOptions?.type || \"default\",\n      timeout: jsxOptions?.duration || undefined,\n      priority: jsxOptions?.priority || \"low\",\n      ...(jsxOptions?.action && {\n        actionProps: {\n          children: jsxOptions.action.label,\n          onClick: jsxOptions.action.onClick,\n        },\n      }),\n      data: {\n        customJSX: optionsOrJSX,\n        ...(jsxOptions?.data && jsxOptions.data),\n        showCloseButton: jsxOptions?.showCloseButton ?? true,\n      },\n      ...(jsxOptions?.onClose && { onClose: jsxOptions.onClose }),\n      ...(jsxOptions?.onRemove && { onRemove: jsxOptions.onRemove }),\n    });\n  }\n\n  const options = optionsOrJSX as ToastOptions<TData>;\n  return toastManager.add({\n    ...(options.id && { id: options.id }),\n    title: options.title,\n    description: options.description || \"\",\n    type: options.type || \"default\",\n    timeout: options.duration || undefined,\n    priority: options.priority || \"low\",\n    ...(options.action && {\n      actionProps: {\n        children: options.action.label,\n        onClick: options.action.onClick,\n      },\n    }),\n    data: {\n      ...(options.data || {}),\n      showCloseButton: options.showCloseButton ?? true,\n    },\n    ...(options.onClose && { onClose: options.onClose }),\n    ...(options.onRemove && { onRemove: options.onRemove }),\n  });\n}\n\n/** Message format for promise toasts - can be a string or an object with title/description */\ntype PromiseMessage = string | { title?: string; description?: string };\n\n/** Promise message that can also be a function returning the message */\ntype PromiseMessageOrFn<T> = PromiseMessage | ((data: T) => PromiseMessage);\n\nconst promise = async <T,>(\n  promiseToResolve: Promise<T>,\n  messages: {\n    loading: PromiseMessage;\n    success: PromiseMessageOrFn<T>;\n    error: PromiseMessageOrFn<Error>;\n  },\n) => {\n  const resolveMessage = <U,>(msg: PromiseMessageOrFn<U>, data: U) =>\n    typeof msg === \"function\" ? msg(data) : msg;\n\n  return toastManager.promise(promiseToResolve, {\n    loading: messages.loading,\n    success: (data: T) => resolveMessage(messages.success, data),\n    error: (err: Error) => resolveMessage(messages.error, err),\n  });\n};\n\nfunction createTypedToast(type: NonNullable<ToastOptions[\"type\"]>) {\n  return <TData extends object = object>(\n    optionsOrJSX: Omit<ToastOptions<TData>, \"type\"> | React.ReactElement,\n    jsxOptions?: Omit<ToastOptions<TData>, \"title\" | \"description\" | \"type\">,\n  ) => {\n    if (React.isValidElement(optionsOrJSX)) {\n      return baseToast(optionsOrJSX, { ...jsxOptions, type });\n    }\n    return baseToast({ ...optionsOrJSX, type });\n  };\n}\n\n/** The fields `toast.update()` applies; other ToastOptions are set at creation only. */\nexport type ToastUpdateOptions = Pick<\n  ToastOptions,\n  \"title\" | \"description\" | \"type\" | \"data\"\n>;\n\nfunction toUpdateOptions(\n  next: ToastUpdateOptions,\n  prevToast: Toast.Root.ToastObject,\n): Record<string, unknown> {\n  const updateOptions: Record<string, unknown> = {};\n  if (next.title !== undefined) updateOptions.title = next.title;\n  if (next.description !== undefined)\n    updateOptions.description = next.description;\n  if (next.type !== undefined) updateOptions.type = next.type;\n  // Base UI replaces `data` wholesale; merge so internal keys (showCloseButton, customJSX) survive.\n  if (next.data !== undefined)\n    updateOptions.data = { ...prevToast.data, ...next.data };\n  return updateOptions;\n}\n\nexport const toast = Object.assign(baseToast, {\n  success: createTypedToast(\"success\"),\n  error: createTypedToast(\"error\"),\n  warning: createTypedToast(\"warning\"),\n  info: createTypedToast(\"info\"),\n  promise,\n  dismiss: (toastId: string) => {\n    return toastManager.close(toastId);\n  },\n  /** Pass a function to derive the update from the current toast (e.g. incrementing a count in `data`). */\n  update: (\n    toastId: string,\n    options:\n      | ToastUpdateOptions\n      | ((prevToast: Toast.Root.ToastObject) => ToastUpdateOptions),\n  ) => {\n    return toastManager.update(toastId, (prevToast) =>\n      toUpdateOptions(\n        typeof options === \"function\" ? options(prevToast) : options,\n        prevToast,\n      ),\n    );\n  },\n  /** Show an anchored toast near an element */\n  anchored: <TData extends object = object>(\n    options: AnchoredToastOptions<TData>,\n  ) => {\n    const anchor =\n      options.anchor instanceof Element\n        ? options.anchor\n        : options.anchor?.current;\n\n    if (!anchor) {\n      console.warn(\"Toast anchor element not found\");\n      return;\n    }\n\n    return anchoredToastManager.add({\n      title: options.title,\n      description: options.description || \"\",\n      timeout: options.duration || undefined,\n      priority: options.priority || \"low\",\n      ...(options.action && {\n        actionProps: {\n          children: options.action.label,\n          onClick: options.action.onClick,\n        },\n      }),\n      data: {\n        ...(options.data || {}),\n        arrow: options.arrow ?? false,\n      },\n      positionerProps: {\n        anchor,\n        side: options.side ?? \"top\",\n        sideOffset: options.sideOffset ?? 8,\n        align: options.align,\n        alignOffset: options.alignOffset,\n      },\n      ...(options.onClose && { onClose: options.onClose }),\n      ...(options.onRemove && { onRemove: options.onRemove }),\n    });\n  },\n  /** Dismiss an anchored toast */\n  dismissAnchored: (toastId: string) => {\n    return anchoredToastManager.close(toastId);\n  },\n  /** Show a grouped toast that collapses multiple items into a summary */\n  grouped: <TData extends object = object>(\n    options: GroupedToastOptions<TData>,\n  ) => {\n    const existingToastId = groupToToastMap.get(options.groupId);\n    const itemId = `grouped-${options.groupId}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;\n\n    const isLoading = options.type === \"loading\";\n\n    const newItem: GroupedToastItem = {\n      id: itemId,\n      title: options.title,\n      description: options.description,\n      type: options.type,\n      action: options.action,\n      data: options.data,\n      createdAt: Date.now(),\n      showCloseButton: options.showCloseButton ?? true,\n      // For loading items, duration controls progress bar time after success\n      duration: isLoading ? options.duration : undefined,\n    };\n\n    if (existingToastId) {\n      const existingData = groupDataMap.get(options.groupId);\n      if (existingData) {\n        const updatedHistoricalCounts = { ...existingData.historicalCounts };\n        if (\n          newItem.type === \"success\" ||\n          newItem.type === \"error\" ||\n          newItem.type === \"warning\" ||\n          newItem.type === \"info\"\n        ) {\n          updatedHistoricalCounts[newItem.type]++;\n        }\n        const updatedData: GroupedToastData = {\n          ...existingData,\n          items: [...existingData.items, newItem],\n          historicalCounts: updatedHistoricalCounts,\n        };\n        groupDataMap.set(options.groupId, updatedData);\n        // Pass timeout to reset Base UI's built-in timer (pauses on hover/drag)\n        toastManager.update(existingToastId, {\n          data: updatedData,\n          timeout: existingData.duration ?? 0,\n        });\n      }\n    } else {\n      const groupId = options.groupId;\n      const groupData: GroupedToastData = {\n        isGrouped: true,\n        groupId,\n        items: [newItem],\n        completedItems: [],\n        isExpanded: false,\n        summary: options.groupSummary,\n        action: {\n          label: options.groupAction?.label ?? \"Show\",\n          expandedLabel: options.groupAction?.expandedLabel ?? \"Hide\",\n        },\n        hasShownAllComplete: false,\n        // For non-loading items, duration controls group auto-dismiss\n        // For loading items, duration is stored on the item for progress bar\n        duration: isLoading ? undefined : options.duration,\n        historicalCounts: {\n          success: newItem.type === \"success\" ? 1 : 0,\n          error: newItem.type === \"error\" ? 1 : 0,\n          warning: newItem.type === \"warning\" ? 1 : 0,\n          info: newItem.type === \"info\" ? 1 : 0,\n        },\n      };\n      const toastId = toastManager.add({\n        title: \"\",\n        description: \"\",\n        data: groupData,\n        // Use Base UI's timeout for auto-dismiss; loading items pass 0 (wait for updateGroupItem).\n        timeout: isLoading ? 0 : (options.duration ?? 0),\n        onClose: () => {\n          const data = groupDataMap.get(groupId);\n          if (data) {\n            data.items.forEach((item) => groupItemToGroupMap.delete(item.id));\n            (data.completedItems ?? []).forEach((item) =>\n              groupItemToGroupMap.delete(item.id),\n            );\n          }\n          groupToToastMap.delete(groupId);\n          groupDataMap.delete(groupId);\n        },\n      });\n      if (toastId) {\n        groupToToastMap.set(groupId, toastId);\n        groupDataMap.set(groupId, groupData);\n      }\n    }\n\n    groupItemToGroupMap.set(itemId, options.groupId);\n\n    return itemId;\n  },\n  /** Dismiss a single item from a grouped toast (checks both pending and completed arrays) */\n  dismissGroupItem: (itemId: string) => {\n    const groupId = groupItemToGroupMap.get(itemId);\n    if (!groupId) return;\n\n    const toastId = groupToToastMap.get(groupId);\n    if (!toastId) return;\n\n    const data = groupDataMap.get(groupId);\n    if (!data) return;\n\n    const inPending = data.items.some((item) => item.id === itemId);\n    const inCompleted = (data.completedItems ?? []).some(\n      (item) => item.id === itemId,\n    );\n\n    let newItems = data.items;\n    let newCompletedItems = data.completedItems ?? [];\n\n    if (inPending) {\n      newItems = data.items.filter((item) => item.id !== itemId);\n    }\n    if (inCompleted) {\n      newCompletedItems = newCompletedItems.filter(\n        (item) => item.id !== itemId,\n      );\n    }\n\n    groupItemToGroupMap.delete(itemId);\n\n    if (newItems.length === 0 && newCompletedItems.length === 0) {\n      toastManager.close(toastId);\n      groupToToastMap.delete(groupId);\n      groupDataMap.delete(groupId);\n    } else {\n      const updatedData: GroupedToastData = {\n        ...data,\n        items: newItems,\n        completedItems: newCompletedItems,\n        // Collapse if only 1 pending item remains and no completed\n        isExpanded:\n          newItems.length <= 1 && newCompletedItems.length === 0\n            ? false\n            : data.isExpanded,\n      };\n      groupDataMap.set(groupId, updatedData);\n      toastManager.update(toastId, { data: updatedData });\n    }\n  },\n  /** Dismiss an entire group of toasts */\n  dismissGroup: (groupId: string) => {\n    const toastId = groupToToastMap.get(groupId);\n    if (!toastId) return;\n\n    const data = groupDataMap.get(groupId);\n    if (data) {\n      data.items.forEach((item) => groupItemToGroupMap.delete(item.id));\n      (data.completedItems ?? []).forEach((item) =>\n        groupItemToGroupMap.delete(item.id),\n      );\n    }\n\n    toastManager.close(toastId);\n    groupToToastMap.delete(groupId);\n    groupDataMap.delete(groupId);\n  },\n  /** Update a single item in a grouped toast */\n  updateGroupItem: (\n    itemId: string,\n    options: Partial<Omit<GroupedToastItem, \"id\" | \"createdAt\">>,\n  ) => {\n    const groupId = groupItemToGroupMap.get(itemId);\n    if (!groupId) return;\n\n    const toastId = groupToToastMap.get(groupId);\n    if (!toastId) return;\n\n    const data = groupDataMap.get(groupId);\n    if (!data) return;\n\n    const currentItem = data.items.find((item) => item.id === itemId);\n    const wasLoading = currentItem?.type === \"loading\";\n    const isNowComplete =\n      options.type !== undefined && options.type !== \"loading\";\n\n    if (wasLoading && isNowComplete && currentItem) {\n      // Move item from pending to completed\n      const newItems = data.items.filter((item) => item.id !== itemId);\n      const completedItem: GroupedToastItem = {\n        ...currentItem,\n        ...options,\n        completedAt: Date.now(),\n      };\n\n      // Insert at beginning (newest on top)\n      const newCompletedItems = [completedItem, ...(data.completedItems ?? [])];\n\n      const completeCount = newCompletedItems.length;\n      const shouldShowAllComplete = completeCount >= 2;\n\n      // Increment historical counts (these persist even after items are dismissed)\n      const completedType = completedItem.type as ToastType;\n      const newHistoricalCounts = { ...data.historicalCounts };\n      if (completedType === \"success\") newHistoricalCounts.success++;\n      else if (completedType === \"error\") newHistoricalCounts.error++;\n      else if (completedType === \"warning\") newHistoricalCounts.warning++;\n      else if (completedType === \"info\") newHistoricalCounts.info++;\n      // \"default\" is the unstyled/neutral variant - count as success\n      else if (completedType === \"default\") newHistoricalCounts.success++;\n\n      const updatedData: GroupedToastData = {\n        ...data,\n        items: newItems,\n        completedItems: newCompletedItems,\n        hasShownAllComplete: data.hasShownAllComplete || shouldShowAllComplete,\n        historicalCounts: newHistoricalCounts,\n      };\n      groupDataMap.set(groupId, updatedData);\n\n      const dismissDuration =\n        currentItem.duration ?? GROUP_ITEM_DISMISS_DURATION;\n\n      if (newItems.length === 0) {\n        // Last pending item — use Base UI's native timer (pauses on drag/hover).\n        toastManager.update(toastId, {\n          data: updatedData,\n          timeout: dismissDuration,\n        });\n      } else {\n        // Still pending items remain — Base UI timer can't be per-item, use setTimeout.\n        toastManager.update(toastId, { data: updatedData });\n        setTimeout(() => {\n          toast.dismissCompletedItem(itemId);\n        }, dismissDuration);\n      }\n    } else {\n      const newItems = data.items.map((item) =>\n        item.id === itemId ? { ...item, ...options } : item,\n      );\n\n      const updatedData: GroupedToastData = {\n        ...data,\n        items: newItems,\n      };\n      groupDataMap.set(groupId, updatedData);\n      toastManager.update(toastId, { data: updatedData });\n    }\n  },\n  /** Dismiss a completed item (removes from completedItems array) */\n  dismissCompletedItem: (itemId: string) => {\n    const groupId = groupItemToGroupMap.get(itemId);\n    if (!groupId) return;\n\n    const toastId = groupToToastMap.get(groupId);\n    if (!toastId) return;\n\n    const data = groupDataMap.get(groupId);\n    if (!data) return;\n\n    const newCompletedItems = (data.completedItems ?? []).filter(\n      (item) => item.id !== itemId,\n    );\n    groupItemToGroupMap.delete(itemId);\n\n    // Check if entire group should close\n    if (data.items.length === 0 && newCompletedItems.length === 0) {\n      toastManager.close(toastId);\n      groupToToastMap.delete(groupId);\n      groupDataMap.delete(groupId);\n      return;\n    }\n\n    const updatedData: GroupedToastData = {\n      ...data,\n      completedItems: newCompletedItems,\n      // Collapse if only 1 pending item remains and no completed (unless in group mode)\n      isExpanded:\n        data.items.length <= 1 &&\n        newCompletedItems.length === 0 &&\n        !data.hasShownAllComplete\n          ? false\n          : data.isExpanded,\n    };\n    groupDataMap.set(groupId, updatedData);\n    toastManager.update(toastId, { data: updatedData });\n  },\n  /** Show a grouped toast that resolves with a promise, handling loading/success/error states */\n  groupedPromise: async <T,>(\n    promiseToResolve: Promise<T>,\n    messages: GroupedPromiseMessages<T>,\n    options: GroupedPromiseOptions,\n  ): Promise<T> => {\n    const resolveMessage = <U,>(\n      msg: PromiseMessage | ((data: U) => GroupedPromiseMessage),\n      data: U,\n    ): GroupedPromiseMessage => {\n      if (typeof msg === \"function\") {\n        return msg(data);\n      }\n      if (typeof msg === \"string\") {\n        return { title: msg };\n      }\n      return msg;\n    };\n\n    const itemId = toast.grouped({\n      ...options,\n      title: messages.loading.title,\n      description: messages.loading.description,\n      type: \"loading\",\n      action: messages.loading.action,\n    });\n\n    if (!itemId) {\n      return promiseToResolve;\n    }\n\n    let handled = false;\n    let wasAborted = false;\n\n    if (options.signal) {\n      const handleAbort = () => {\n        if (handled) return;\n        handled = true;\n        wasAborted = true;\n\n        const abortedMsg: GroupedPromiseMessage = messages.aborted\n          ? typeof messages.aborted === \"function\"\n            ? messages.aborted()\n            : typeof messages.aborted === \"string\"\n              ? { title: messages.aborted }\n              : messages.aborted\n          : { title: \"Cancelled\" };\n\n        toast.updateGroupItem(itemId, {\n          title: abortedMsg.title,\n          description: abortedMsg.description,\n          type: abortedMsg.type ?? \"error\",\n          action: undefined,\n        });\n      };\n\n      if (options.signal.aborted) {\n        handleAbort();\n        throw new DOMException(\"Aborted\", \"AbortError\");\n      }\n\n      options.signal.addEventListener(\"abort\", handleAbort, { once: true });\n    }\n\n    try {\n      const result = await promiseToResolve;\n\n      if (!handled) {\n        handled = true;\n        const successMsg = resolveMessage(messages.success, result);\n        toast.updateGroupItem(itemId, {\n          title: successMsg.title,\n          description: successMsg.description,\n          type: successMsg.type ?? \"success\",\n          action: undefined,\n        });\n      }\n\n      return result;\n    } catch (error) {\n      if (!handled) {\n        handled = true;\n        const errorMsg = resolveMessage(\n          messages.error,\n          error instanceof Error ? error : new Error(String(error)),\n        );\n        toast.updateGroupItem(itemId, {\n          title: errorMsg.title,\n          description: errorMsg.description,\n          type: errorMsg.type ?? \"error\",\n          action: undefined,\n        });\n      }\n\n      // Re-throw as AbortError to match fetch() behavior (error.name === 'AbortError').\n      if (wasAborted) {\n        throw new DOMException(\"Aborted\", \"AbortError\");\n      }\n\n      throw error;\n    }\n  },\n});\n\nexport type ToastPosition =\n  | \"top-left\"\n  | \"top-center\"\n  | \"top-right\"\n  | \"bottom-left\"\n  | \"bottom-center\"\n  | \"bottom-right\";\n\ninterface ToastProviderProps {\n  children: React.ReactNode;\n  position?: ToastPosition;\n  limit?: number;\n  timeout?: number;\n  container?: HTMLElement | React.RefObject<HTMLElement | null> | null;\n}\n\nexport function ToastProvider({\n  children,\n  position = \"bottom-right\",\n  limit = 3,\n  timeout = 5000,\n  container,\n}: ToastProviderProps) {\n  return (\n    <Toast.Provider limit={limit} timeout={timeout} toastManager={toastManager}>\n      {children}\n      <Toast.Portal container={container}>\n        <Toast.Viewport\n          data-slot=\"toast-viewport\"\n          data-position={position}\n          className={cn(\n            \"fixed z-50 flex w-[calc(100%-var(--toast-inset)*2)] max-w-[360px]\",\n            \"[--toast-inset:1rem] sm:[--toast-inset:2rem]\",\n            // Vertical positioning\n            \"data-[position*=top]:top-(--toast-inset)\",\n            \"data-[position*=bottom]:bottom-(--toast-inset)\",\n            // Horizontal positioning\n            \"data-[position*=left]:left-(--toast-inset)\",\n            \"data-[position*=right]:right-(--toast-inset)\",\n            \"data-[position*=center]:left-1/2 data-[position*=center]:-translate-x-1/2\",\n          )}\n        >\n          <StackedToasts position={position} />\n        </Toast.Viewport>\n      </Toast.Portal>\n    </Toast.Provider>\n  );\n}\n\nfunction StackedToasts({ position }: { position: ToastPosition }) {\n  const { toasts } = Toast.useToastManager();\n  const swipeDirection = getSwipeDirection(position);\n\n  return (\n    <>\n      {toasts.map((toast) => (\n        <StackedToastItem\n          key={toast.id}\n          toast={toast as ToastData}\n          position={position}\n          swipeDirection={swipeDirection}\n        />\n      ))}\n    </>\n  );\n}\n\ninterface StackedToastItemProps {\n  toast: ToastData;\n  position: ToastPosition;\n  swipeDirection: SwipeDirection[];\n}\n\nfunction StackedToastItem({\n  toast,\n  position,\n  swipeDirection,\n}: StackedToastItemProps) {\n  const isGrouped =\n    toast.data &&\n    typeof toast.data === \"object\" &&\n    \"isGrouped\" in toast.data &&\n    (toast.data as GroupedToastData).isGrouped === true;\n\n  if (isGrouped) {\n    return (\n      <GroupedToastRoot\n        toast={toast}\n        position={position}\n        swipeDirection={swipeDirection}\n        data={toast.data as GroupedToastData}\n      />\n    );\n  }\n\n  const type = toast.type || \"default\";\n\n  const hasCustomJSX =\n    toast.data && typeof toast.data === \"object\" && \"customJSX\" in toast.data;\n\n  const showCloseButton =\n    toast.data &&\n    typeof toast.data === \"object\" &&\n    \"showCloseButton\" in toast.data &&\n    (toast.data as Record<string, unknown>).showCloseButton === false\n      ? false\n      : true;\n\n  const Icon =\n    type !== \"default\" ? TOAST_ICONS[type as keyof typeof TOAST_ICONS] : null;\n\n  return (\n    <Toast.Root\n      toast={toast}\n      swipeDirection={swipeDirection}\n      data-slot=\"toast\"\n      data-position={position}\n      className={cn(\n        TOAST_ROOT_CLASSES,\n        // Deduplicated toast pulse\n        upsertReplayClassName(toast),\n      )}\n    >\n      <Toast.Content\n        data-slot=\"toast-content\"\n        className={cn(\n          \"flex gap-3 overflow-hidden px-3.5 py-3 text-sm\",\n          showCloseButton ? \"items-start\" : \"items-center\",\n          \"transition-opacity duration-250\",\n          \"data-behind:pointer-events-none data-behind:opacity-0\",\n          \"data-expanded:pointer-events-auto data-expanded:opacity-100\",\n        )}\n      >\n        {hasCustomJSX ? (\n          <div className=\"w-full\">\n            {(toast.data as Record<string, React.ReactNode>).customJSX}\n          </div>\n        ) : (\n          <>\n            {Icon && (\n              <div\n                data-slot=\"toast-icon\"\n                className={cn(\n                  \"[&>svg]:size-4 [&>svg]:shrink-0\",\n                  showCloseButton && \"mt-0.5\",\n                )}\n              >\n                <HugeiconsIcon\n                  icon={Icon}\n                  strokeWidth={2}\n                  className={cn(\n                    \"in-data-[type=success]:text-success-foreground\",\n                    \"in-data-[type=error]:text-danger-foreground\",\n                    \"in-data-[type=warning]:text-warning-foreground\",\n                    \"in-data-[type=info]:text-info-foreground\",\n                    \"in-data-[type=loading]:text-muted-foreground in-data-[type=loading]:animate-spin\",\n                  )}\n                />\n              </div>\n            )}\n            <div className=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n              <Toast.Title\n                data-slot=\"toast-title\"\n                className=\"text-sm leading-5 font-medium\"\n              />\n              <Toast.Description\n                data-slot=\"toast-description\"\n                className=\"text-muted-foreground text-sm leading-5\"\n              />\n              {showCloseButton && (\n                <Toast.Action\n                  data-slot=\"toast-action\"\n                  render={<Button variant=\"outline\" size=\"xs\" />}\n                  className=\"mt-1.5 w-fit\"\n                />\n              )}\n            </div>\n            {!showCloseButton && (\n              <Toast.Action\n                data-slot=\"toast-action\"\n                render={<Button variant=\"outline\" size=\"xs\" />}\n              />\n            )}\n            {showCloseButton && (\n              <Toast.Close\n                data-slot=\"toast-close\"\n                className=\"text-muted-foreground hover:bg-surface-hover hover:text-foreground -mt-1 -mr-1 flex size-6 shrink-0 items-center justify-center rounded-md border-none bg-transparent transition-colors duration-200\"\n                aria-label=\"Close\"\n              >\n                <HugeiconsIcon\n                  icon={Cancel01Icon}\n                  strokeWidth={2}\n                  className=\"size-4\"\n                />\n              </Toast.Close>\n            )}\n          </>\n        )}\n      </Toast.Content>\n    </Toast.Root>\n  );\n}\n\ninterface AnchoredToastProviderProps {\n  children: React.ReactNode;\n  limit?: number;\n  timeout?: number;\n  container?: HTMLElement | React.RefObject<HTMLElement | null> | null;\n}\n\nexport function AnchoredToastProvider({\n  children,\n  limit = 5,\n  timeout = 2000,\n  container,\n}: AnchoredToastProviderProps) {\n  return (\n    <Toast.Provider\n      limit={limit}\n      timeout={timeout}\n      toastManager={anchoredToastManager}\n    >\n      {children}\n      <Toast.Portal container={container}>\n        <Toast.Viewport data-slot=\"toast-viewport\" className=\"outline-none\">\n          <AnchoredToasts />\n        </Toast.Viewport>\n      </Toast.Portal>\n    </Toast.Provider>\n  );\n}\n\nfunction AnchoredToasts() {\n  const { toasts } = Toast.useToastManager();\n\n  return (\n    <>\n      {toasts.map((toast) => {\n        const toastData = toast as ToastData;\n        if (!toastData.positionerProps?.anchor) {\n          return null;\n        }\n        return <AnchoredToastItem key={toast.id} toast={toastData} />;\n      })}\n    </>\n  );\n}\n\nfunction AnchoredToastItem({ toast }: { toast: ToastData }) {\n  const showArrow = Boolean(\n    toast.data &&\n    typeof toast.data === \"object\" &&\n    \"arrow\" in toast.data &&\n    (toast.data as Record<string, unknown>).arrow === true,\n  );\n\n  return (\n    <Toast.Positioner\n      toast={toast}\n      data-slot=\"toast-positioner\"\n      className=\"z-[calc(1000-var(--toast-index))]\"\n    >\n      <Toast.Root\n        toast={toast}\n        data-slot=\"toast\"\n        className={cn(\n          \"flex w-max origin-(--transform-origin) flex-col rounded-md\",\n          // Surface elevation — level=3 (tooltip/popover tier), shadow matches.\n          solidSurface(3, 3),\n          \"text-popover-foreground\",\n          \"px-3 py-2 text-sm\",\n          \"transition-all duration-200\",\n          \"data-starting-style:scale-95 data-starting-style:opacity-0\",\n          \"data-ending-style:scale-95 data-ending-style:opacity-0\",\n        )}\n      >\n        {showArrow && (\n          <Toast.Arrow\n            data-slot=\"toast-arrow\"\n            className=\"[&>path:not(:first-child)]:stroke-border fill-(--popup-surface,var(--card)) [&>path:first-child]:fill-(--popup-surface,var(--card))\"\n          />\n        )}\n        <Toast.Content data-slot=\"toast-content\">\n          <Toast.Title\n            data-slot=\"toast-title\"\n            className=\"text-sm font-medium\"\n          />\n          <Toast.Description\n            data-slot=\"toast-description\"\n            className=\"text-muted-foreground text-sm\"\n          />\n        </Toast.Content>\n      </Toast.Root>\n    </Toast.Positioner>\n  );\n}\n\n// Feature detection for calc-size() (Chrome 129+, Edge 129+).\n// Used to conditionally render different DOM structures for height animation.\nconst supportsCalcSize =\n  typeof CSS !== \"undefined\" && CSS.supports(\"height\", \"calc-size(auto, size)\");\n\ninterface GroupedToastRootProps {\n  toast: ToastData;\n  position: ToastPosition;\n  swipeDirection: SwipeDirection[];\n  data: GroupedToastData;\n}\n\n/** Toggle expand/collapse for a grouped toast. */\nfunction toggleGroupExpanded(toastId: string) {\n  let groupId: string | undefined;\n  for (const [gId, tId] of groupToToastMap.entries()) {\n    if (tId === toastId) {\n      groupId = gId;\n      break;\n    }\n  }\n  if (!groupId) return;\n\n  const data = groupDataMap.get(groupId);\n  if (!data) return;\n\n  const updatedData: GroupedToastData = {\n    ...data,\n    isExpanded: !data.isExpanded,\n  };\n  groupDataMap.set(groupId, updatedData);\n  toastManager.update(toastId, { data: updatedData });\n}\n\nfunction GroupedToastRoot({\n  toast,\n  position,\n  swipeDirection,\n  data,\n}: GroupedToastRootProps) {\n  const toastRootRef = React.useRef<HTMLDivElement>(null);\n  const isTop = position.startsWith(\"top\");\n\n  // Sync popover open state with toast data\n  const handlePopoverOpenChange = React.useCallback(\n    (open: boolean, eventDetails: Popover.Root.ChangeEventDetails) => {\n      // Only allow escape-key to close; all other closes are managed by the Show/Hide button.\n      if (!open && eventDetails.reason !== \"escape-key\") {\n        eventDetails.cancel();\n        return;\n      }\n      if (open !== data.isExpanded) {\n        toggleGroupExpanded(toast.id);\n      }\n    },\n    [toast.id, data.isExpanded],\n  );\n\n  return (\n    <Toast.Root\n      ref={toastRootRef}\n      toast={toast}\n      swipeDirection={swipeDirection}\n      data-slot=\"toast\"\n      data-position={position}\n      className={cn(\n        TOAST_ROOT_CLASSES,\n        // overflow-visible required — the expanded popover renders outside the toast's bounds.\n        \"overflow-visible\",\n      )}\n    >\n      <Toast.Content\n        data-slot=\"toast-content\"\n        className={cn(\n          \"text-sm\",\n          \"transition-[opacity,height] duration-300\",\n          \"data-behind:pointer-events-none data-behind:opacity-0\",\n          \"data-expanded:pointer-events-auto data-expanded:opacity-100\",\n        )}\n      >\n        <GroupedToastSummaryOrSingle data={data} toastId={toast.id} />\n      </Toast.Content>\n      {/* Expanded cards popover - uses CSS positioning to maintain stable layout */}\n      <Popover.Root\n        open={data.isExpanded}\n        onOpenChange={handlePopoverOpenChange}\n      >\n        <Popover.Portal container={toastRootRef}>\n          <Popover.Positioner\n            anchor={toastRootRef}\n            side={isTop ? \"bottom\" : \"top\"}\n            align=\"start\"\n            sideOffset={8}\n          >\n            <Popover.Popup\n              data-slot=\"expanded-cards-popup\"\n              className={cn(\n                \"absolute w-(--anchor-width)\",\n                // align=\"start\" sets origin to the left edge; override to center.\n                isTop\n                  ? \"top-full mt-2 origin-top\"\n                  : \"bottom-full mb-2 origin-bottom\",\n                // Only transition opacity and scale for enter/exit\n                \"ease-out-expo transition-[opacity,scale] duration-200\",\n                \"data-starting-style:scale-95 data-starting-style:opacity-0\",\n                \"data-ending-style:scale-95 data-ending-style:opacity-0\",\n              )}\n            >\n              <ExpandedCardsContainer data={data} isTop={isTop} />\n            </Popover.Popup>\n          </Popover.Positioner>\n        </Popover.Portal>\n      </Popover.Root>\n    </Toast.Root>\n  );\n}\n\ninterface GroupedToastSummaryOrSingleProps {\n  data: GroupedToastData;\n  toastId: string;\n}\n\nfunction GroupedToastSummaryOrSingle({\n  data,\n  toastId,\n}: GroupedToastSummaryOrSingleProps) {\n  const totalPending = data.items.length;\n  const totalCompleted = (data.completedItems ?? []).length;\n  const totalItems = totalPending + totalCompleted;\n\n  const isSingle = totalItems === 1 && !data.hasShownAllComplete;\n  const singleItem = data.items[0] ?? data.completedItems?.[0];\n\n  // Chrome 129+: single element with key-swap; height animates via calc-size(auto, size).\n  if (supportsCalcSize) {\n    return (\n      <div\n        key={isSingle ? \"single\" : \"summary\"}\n        className={cn(\n          \"ease-out-expo duration-200\",\n          \"transition-[height,opacity,filter,scale]\",\n          \"overflow-clip\",\n          // Start from the known CSS var height, animate to intrinsic auto size.\n          \"h-[calc-size(auto,size)]\",\n          \"starting:h-(--toast-calc-height)\",\n          \"starting:scale-95 starting:opacity-0 starting:blur-[2px]\",\n          \"blur-0 scale-100 opacity-100\",\n        )}\n      >\n        {isSingle && singleItem ? (\n          <GroupedSingleItemContent\n            item={singleItem}\n            showCloseButton={singleItem.showCloseButton}\n          />\n        ) : (\n          <GroupedToastSummaryContent\n            data={data}\n            onToggle={() => toggleGroupExpanded(toastId)}\n          />\n        )}\n      </div>\n    );\n  }\n\n  // Fallback (Firefox/Safari): two elements with grid-row height animation.\n  return (\n    <>\n      <div\n        className={cn(\n          \"ease-out-expo grid duration-200\",\n          \"transition-[grid-template-rows,opacity,filter,scale]\",\n          isSingle\n            ? \"blur-0 scale-100 grid-rows-[1fr] opacity-100\"\n            : \"pointer-events-none scale-95 grid-rows-[0fr] opacity-0 blur-[2px]\",\n        )}\n      >\n        <div className=\"overflow-hidden\">\n          {singleItem && (\n            <GroupedSingleItemContent\n              item={singleItem}\n              showCloseButton={singleItem.showCloseButton}\n            />\n          )}\n        </div>\n      </div>\n\n      <div\n        className={cn(\n          \"ease-out-expo grid duration-200\",\n          \"transition-[grid-template-rows,opacity,filter,scale]\",\n          !isSingle\n            ? \"blur-0 scale-100 grid-rows-[1fr] opacity-100\"\n            : \"pointer-events-none scale-95 grid-rows-[0fr] opacity-0 blur-[2px]\",\n        )}\n      >\n        <div className=\"overflow-hidden\">\n          <GroupedToastSummaryContent\n            data={data}\n            onToggle={() => toggleGroupExpanded(toastId)}\n          />\n        </div>\n      </div>\n    </>\n  );\n}\n\ninterface GroupedSingleItemContentProps {\n  item: GroupedToastItem;\n  showCloseButton?: boolean;\n}\n\nfunction GroupedSingleItemContent({\n  item,\n  showCloseButton = true,\n}: GroupedSingleItemContentProps) {\n  const type = item.type || \"default\";\n  const Icon =\n    type !== \"default\" ? TOAST_ICONS[type as keyof typeof TOAST_ICONS] : null;\n\n  return (\n    <div\n      className={cn(\n        \"flex gap-3 px-3.5 py-3\",\n        showCloseButton ? \"items-start\" : \"items-center\",\n      )}\n    >\n      {Icon && (\n        <div\n          data-slot=\"toast-icon\"\n          className={cn(\n            \"[&>svg]:size-4 [&>svg]:shrink-0\",\n            showCloseButton && \"mt-0.5\",\n          )}\n        >\n          <HugeiconsIcon\n            icon={Icon}\n            strokeWidth={2}\n            className={toastIconVariants({ type })}\n          />\n        </div>\n      )}\n      <div className=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n        {item.title && (\n          <span className=\"text-sm leading-5 font-medium\">{item.title}</span>\n        )}\n        {item.description && (\n          <span className=\"text-muted-foreground text-sm leading-5\">\n            {item.description}\n          </span>\n        )}\n        {showCloseButton && item.action && (\n          <div className=\"mt-1.5\">\n            <Button variant=\"outline\" size=\"xs\" onClick={item.action.onClick}>\n              {item.action.label}\n            </Button>\n          </div>\n        )}\n      </div>\n      {!showCloseButton && item.action && (\n        <Button variant=\"outline\" size=\"xs\" onClick={item.action.onClick}>\n          {item.action.label}\n        </Button>\n      )}\n      {showCloseButton && (\n        <Toast.Close\n          data-slot=\"toast-close\"\n          className=\"text-muted-foreground hover:bg-surface-hover hover:text-foreground -mt-1 -mr-1 flex size-6 shrink-0 items-center justify-center rounded-md border-none bg-transparent transition-colors duration-200\"\n          aria-label=\"Close\"\n        >\n          <HugeiconsIcon\n            icon={Cancel01Icon}\n            strokeWidth={2}\n            className=\"size-4\"\n          />\n        </Toast.Close>\n      )}\n    </div>\n  );\n}\n\ninterface GroupedToastSummaryContentProps {\n  data: GroupedToastData;\n  onToggle: () => void;\n}\n\nfunction GroupedToastSummaryContent({\n  data,\n  onToggle,\n}: GroupedToastSummaryContentProps) {\n  const loadingCount = data.items.filter(\n    (item) => item.type === \"loading\",\n  ).length;\n\n  const {\n    success: successCount,\n    error: errorCount,\n    warning: warningCount,\n    info: infoCount,\n  } = data.historicalCounts;\n  const completedCount = successCount + errorCount + warningCount + infoCount;\n  const totalCount = loadingCount + completedCount;\n\n  let iconType: \"loading\" | \"success\" | \"error\" | \"warning\";\n  if (loadingCount > 0) {\n    iconType = \"loading\";\n  } else if (errorCount === 0) {\n    iconType = \"success\"; // All succeeded\n  } else if (successCount === 0 && warningCount === 0 && infoCount === 0) {\n    iconType = \"error\"; // All failed\n  } else {\n    iconType = \"warning\"; // Mixed results\n  }\n  const Icon = TOAST_ICONS[iconType];\n\n  const summaryText =\n    typeof data.summary === \"function\"\n      ? data.summary({\n          loadingCount,\n          completedCount,\n          totalCount,\n          successCount,\n          errorCount,\n          warningCount,\n          infoCount,\n        })\n      : data.summary;\n\n  const buttonLabel = data.isExpanded\n    ? data.action.expandedLabel\n    : data.action.label;\n\n  return (\n    <div className=\"flex items-center gap-3 px-3.5 py-3\">\n      <div data-slot=\"toast-icon\" className=\"[&>svg]:size-4 [&>svg]:shrink-0\">\n        <HugeiconsIcon\n          icon={Icon}\n          strokeWidth={2}\n          className={toastIconVariants({ type: iconType })}\n        />\n      </div>\n      <span className=\"flex-1 font-medium\">{summaryText}</span>\n      {/* Show button when in group mode (historical or current expandable items) */}\n      {(data.hasShownAllComplete ||\n        data.items.length > 1 ||\n        (data.completedItems ?? []).length > 0) && (\n        <Button\n          variant=\"outline\"\n          size=\"xs\"\n          onClick={onToggle}\n          aria-expanded={data.isExpanded}\n        >\n          {buttonLabel}\n        </Button>\n      )}\n    </div>\n  );\n}\n\ninterface ExpandedCardsContainerProps {\n  data: GroupedToastData;\n  isTop: boolean;\n}\n\n/** Container for expanded pending/completed item cards with enter/exit animations */\nfunction ExpandedCardsContainer({ data, isTop }: ExpandedCardsContainerProps) {\n  const completedCount = (data.completedItems ?? []).length;\n  const pendingCount = data.items.length;\n  const hasCompletedItems = completedCount > 0;\n\n  // Show pending card when 2+ pending, or 1 pending alongside completed/historical items.\n  const hasPendingItems =\n    pendingCount > 1 ||\n    (pendingCount >= 1 && hasCompletedItems) ||\n    (pendingCount >= 1 && data.hasShownAllComplete);\n\n  if (!hasCompletedItems && !hasPendingItems) {\n    return null;\n  }\n\n  return (\n    <div\n      data-slot=\"expanded-cards-container\"\n      // flex-col with conditional DOM order (not flex-col-reverse) for Safari AnimatePresence compat.\n      className=\"flex flex-col gap-2\"\n    >\n      <AnimatePresence initial={false} mode=\"popLayout\">\n        {/* Flat conditional rendering (no fragments) required for Safari + AnimatePresence.\n            Completed card renders first for bottom, last for top — so it's always outermost. */}\n\n        {!isTop && hasCompletedItems && (\n          <motion.div\n            key=\"completed-card\"\n            layout\n            initial={{ opacity: 0, scale: 0.95 }}\n            animate={{ opacity: 1, scale: 1 }}\n            exit={{ opacity: 0, scale: 0.95 }}\n            transition={{ duration: 0.2, ease: [0.25, 0.1, 0.25, 1] }}\n          >\n            <CompletedItemsCard items={data.completedItems} isTop={isTop} />\n          </motion.div>\n        )}\n\n        {/* Pending card (always in middle position) */}\n        {hasPendingItems && (\n          <motion.div\n            key=\"pending-card\"\n            layout\n            initial={{ opacity: 0, scale: 0.95 }}\n            animate={{ opacity: 1, scale: 1 }}\n            exit={{ opacity: 0, scale: 0.95, y: -20 }}\n            transition={{ duration: 0.2, ease: [0.25, 0.1, 0.25, 1] }}\n          >\n            <GroupedToastCard data={data} isTop={isTop} />\n          </motion.div>\n        )}\n\n        {/* Completed card last (for top position only) */}\n        {isTop && hasCompletedItems && (\n          <motion.div\n            key=\"completed-card\"\n            layout\n            initial={{ opacity: 0, scale: 0.95 }}\n            animate={{ opacity: 1, scale: 1 }}\n            exit={{ opacity: 0, scale: 0.95 }}\n            transition={{ duration: 0.2, ease: [0.25, 0.1, 0.25, 1] }}\n          >\n            <CompletedItemsCard items={data.completedItems} isTop={isTop} />\n          </motion.div>\n        )}\n      </AnimatePresence>\n    </div>\n  );\n}\n\ninterface GroupedToastCardProps {\n  data: GroupedToastData;\n  isTop: boolean;\n}\n\nfunction GroupedToastCard({ data, isTop }: GroupedToastCardProps) {\n  const cardRef = React.useRef<HTMLDivElement>(null);\n\n  // Capture-phase native listeners prevent touch events from reaching the toast's swipe handler.\n  React.useEffect(() => {\n    const card = cardRef.current;\n    if (!card) return;\n\n    const stopPropagation = (e: TouchEvent) => {\n      e.stopPropagation();\n    };\n\n    card.addEventListener(\"touchstart\", stopPropagation, { passive: true });\n    card.addEventListener(\"touchmove\", stopPropagation, { passive: true });\n    card.addEventListener(\"touchend\", stopPropagation, { passive: true });\n\n    return () => {\n      card.removeEventListener(\"touchstart\", stopPropagation);\n      card.removeEventListener(\"touchmove\", stopPropagation);\n      card.removeEventListener(\"touchend\", stopPropagation);\n    };\n  }, []);\n\n  return (\n    <div\n      ref={cardRef}\n      data-slot=\"grouped-toast-card\"\n      data-base-ui-swipe-ignore\n      className={cn(\n        \"max-h-64 w-full overflow-y-auto overscroll-contain rounded-lg\",\n        // Surface elevation — level=5 because it pops above the level-3 toast.\n        solidSurface(5, 5),\n        \"text-popover-foreground\",\n        \"animate-in fade-in-0 zoom-in-95\",\n        isTop ? \"slide-in-from-top-2\" : \"slide-in-from-bottom-2\",\n      )}\n    >\n      <AnimatePresence initial={false}>\n        {data.items.map((item, index) => (\n          <motion.div\n            key={item.id}\n            initial={{ height: 0, opacity: 0 }}\n            animate={{ height: \"auto\", opacity: 1 }}\n            exit={{ height: 0, opacity: 0 }}\n            transition={{ duration: 0.2, ease: [0.25, 0.1, 0.25, 1] }}\n            className=\"overflow-hidden\"\n          >\n            <GroupedToastCardItem item={item} showSeparator={index > 0} />\n          </motion.div>\n        ))}\n      </AnimatePresence>\n    </div>\n  );\n}\n\ninterface GroupedToastCardItemProps {\n  item: GroupedToastItem;\n  showSeparator: boolean;\n}\n\nfunction GroupedToastCardItem({\n  item,\n  showSeparator,\n}: GroupedToastCardItemProps) {\n  const type = item.type || \"default\";\n  const Icon =\n    type !== \"default\" ? TOAST_ICONS[type as keyof typeof TOAST_ICONS] : null;\n\n  return (\n    <>\n      {showSeparator && (\n        <div\n          className=\"bg-border mx-2 h-px\"\n          data-slot=\"grouped-toast-separator\"\n        />\n      )}\n      <div\n        data-slot=\"grouped-toast-card-item\"\n        className=\"flex items-center gap-3 px-3.5 py-3 text-sm\"\n      >\n        {Icon && (\n          <div\n            data-slot=\"toast-icon\"\n            className=\"[&>svg]:size-4 [&>svg]:shrink-0\"\n          >\n            <HugeiconsIcon\n              icon={Icon}\n              strokeWidth={2}\n              className={toastIconVariants({ type })}\n            />\n          </div>\n        )}\n        <div className=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n          {item.title && (\n            <span className=\"leading-5 font-medium\">{item.title}</span>\n          )}\n          {item.description && (\n            <span className=\"text-muted-foreground leading-5\">\n              {item.description}\n            </span>\n          )}\n        </div>\n        {item.action && (\n          <Button variant=\"outline\" size=\"xs\" onClick={item.action.onClick}>\n            {item.action.label}\n          </Button>\n        )}\n      </div>\n    </>\n  );\n}\n\n// Completed Items Components\ninterface CompletedItemRowProps {\n  item: GroupedToastItem;\n  showSeparator: boolean;\n}\n\nfunction CompletedItemRow({ item, showSeparator }: CompletedItemRowProps) {\n  const type = item.type || \"success\";\n  const Icon =\n    type !== \"default\" ? TOAST_ICONS[type as keyof typeof TOAST_ICONS] : null;\n\n  const dismissDuration = item.duration ?? GROUP_ITEM_DISMISS_DURATION;\n  // Negative delay syncs the CSS animation with the real timer that started at completedAt.\n  const [elapsed] = React.useState(() =>\n    item.completedAt ? Date.now() - item.completedAt : 0,\n  );\n  const animationStyle = {\n    \"--dismiss-duration\": `${dismissDuration}ms`,\n    \"--animation-delay\": `-${Math.min(elapsed, dismissDuration)}ms`,\n  } as React.CSSProperties;\n\n  return (\n    <>\n      {showSeparator && (\n        <div\n          className=\"bg-border h-px w-full\"\n          data-slot=\"completed-item-separator\"\n        />\n      )}\n      <div\n        data-slot=\"completed-item-row\"\n        className=\"relative flex items-center gap-3 overflow-hidden px-3.5 py-3 text-sm\"\n        style={animationStyle}\n      >\n        {/* Progress bar background (fills left-to-right) */}\n        <div\n          data-slot=\"completed-item-progress\"\n          className={cn(\n            \"absolute inset-0 origin-left bg-(--popup-surface,var(--card))\",\n            \"animate-[progress-fill_var(--dismiss-duration)_linear_forwards]\",\n            \"[animation-delay:var(--animation-delay)]\",\n          )}\n        />\n\n        {/* Content layer (above progress bar) */}\n        <div className=\"relative z-10 flex w-full items-center gap-3\">\n          {Icon && (\n            <div\n              data-slot=\"toast-icon\"\n              className=\"[&>svg]:size-4 [&>svg]:shrink-0\"\n            >\n              <HugeiconsIcon\n                icon={Icon}\n                strokeWidth={2}\n                className={toastIconVariants({ type })}\n              />\n            </div>\n          )}\n\n          <div className=\"flex min-w-0 flex-1 flex-col gap-0.5\">\n            {item.title && (\n              <span className=\"leading-5 font-medium\">{item.title}</span>\n            )}\n            {item.description && (\n              <span className=\"text-muted-foreground leading-5\">\n                {item.description}\n              </span>\n            )}\n          </div>\n\n          {item.action && (\n            <Button variant=\"outline\" size=\"xs\" onClick={item.action.onClick}>\n              {item.action.label}\n            </Button>\n          )}\n        </div>\n      </div>\n    </>\n  );\n}\n\ninterface CompletedItemsCardProps {\n  items: GroupedToastItem[];\n  isTop: boolean;\n}\n\nfunction CompletedItemsCard({ items, isTop }: CompletedItemsCardProps) {\n  const cardRef = React.useRef<HTMLDivElement>(null);\n\n  // Same touch isolation as GroupedToastCard.\n  React.useEffect(() => {\n    const card = cardRef.current;\n    if (!card) return;\n\n    const stopPropagation = (e: TouchEvent) => {\n      e.stopPropagation();\n    };\n\n    card.addEventListener(\"touchstart\", stopPropagation, { passive: true });\n    card.addEventListener(\"touchmove\", stopPropagation, { passive: true });\n    card.addEventListener(\"touchend\", stopPropagation, { passive: true });\n\n    return () => {\n      card.removeEventListener(\"touchstart\", stopPropagation);\n      card.removeEventListener(\"touchmove\", stopPropagation);\n      card.removeEventListener(\"touchend\", stopPropagation);\n    };\n  }, []);\n\n  return (\n    <div\n      ref={cardRef}\n      data-slot=\"completed-items-card\"\n      data-base-ui-swipe-ignore\n      className={cn(\n        \"max-h-48 w-full overflow-y-auto overscroll-contain rounded-lg\",\n        // bg-muted is intentionally recessed vs the active card; shadow stays at level=5 to match.\n        \"bg-muted text-popover-foreground\",\n        \"shadow-[var(--surface-shadow-5),var(--surface-rim-5)]\",\n        \"animate-in fade-in-0 zoom-in-95\",\n        isTop ? \"slide-in-from-top-2\" : \"slide-in-from-bottom-2\",\n      )}\n    >\n      <AnimatePresence initial={false}>\n        {items.map((item, index) => (\n          <motion.div\n            key={item.id}\n            initial={{ height: 0, opacity: 0 }}\n            animate={{ height: \"auto\", opacity: 1 }}\n            exit={{ height: 0, opacity: 0 }}\n            transition={{ duration: 0.2, ease: [0.25, 0.1, 0.25, 1] }}\n            className=\"overflow-hidden\"\n          >\n            <CompletedItemRow item={item} showSeparator={index > 0} />\n          </motion.div>\n        ))}\n      </AnimatePresence>\n    </div>\n  );\n}\n\nexport { Toast, toastManager, anchoredToastManager };\n",
      "type": "registry:ui",
      "target": "components/ui/cubby-ui/toast/toast.tsx"
    },
    {
      "path": "registry/default/toast/toast.css",
      "content": "@keyframes progress-fill {\n  from {\n    transform: scaleX(0);\n  }\n  to {\n    transform: scaleX(1);\n  }\n}\n\n/*\n * Paired identical keyframes.\n * CSS animations don't restart when re-applied to the same element with the\n * same animation-name, so on each toast update we alternate between the two\n * names to force the browser to treat it as a new animation and replay it.\n * Keep both bodies in sync — the toggling is what makes the replay work.\n */\n@keyframes pulse-even {\n  0%,\n  100% {\n    scale: 1;\n  }\n  50% {\n    scale: 1.04;\n  }\n}\n\n@keyframes pulse-odd {\n  0%,\n  100% {\n    scale: 1;\n  }\n  50% {\n    scale: 1.04;\n  }\n}\n\n@media (prefers-reduced-motion: reduce) {\n  @keyframes pulse-even {\n    from,\n    to {\n      scale: 1;\n    }\n  }\n\n  @keyframes pulse-odd {\n    from,\n    to {\n      scale: 1;\n    }\n  }\n}\n",
      "type": "registry:file",
      "target": "components/ui/cubby-ui/toast/toast.css"
    }
  ],
  "type": "registry:ui"
}