"use client" import type React from "react" // Adapted from https://github.com/shadcn-ui/ui/blob/main/apps/www/registry/default/ui/use-toast.ts import { useState, useEffect } from "react" import type { ToastActionElement, ToastProps } from "@/components/ui/toast" const TOAST_LIMIT = 5 const TOAST_REMOVE_DELAY = 3000 type ToasterToast = ToastProps & { id: string title?: React.ReactNode description?: React.ReactNode action?: ToastActionElement } const actionTypes = { ADD_TOAST: "ADD_TOAST", UPDATE_TOAST: "UPDATE_TOAST", DISMISS_TOAST: "DISMISS_TOAST", REMOVE_TOAST: "REMOVE_TOAST", } as const let count = 0 function genId() { count = (count + 1) % Number.MAX_VALUE return count.toString() } type ActionType = typeof actionTypes type Action = | { type: ActionType["ADD_TOAST"] toast: ToasterToast } | { type: ActionType["UPDATE_TOAST"] toast: Partial } | { type: ActionType["DISMISS_TOAST"] toastId?: string } | { type: ActionType["REMOVE_TOAST"] toastId?: string } interface State { toasts: ToasterToast[] } const toastTimeouts = new Map>() const reducer = (state: State, action: Action): State => { switch (action.type) { case actionTypes.ADD_TOAST: return { ...state, toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), } case actionTypes.UPDATE_TOAST: return { ...state, toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)), } case actionTypes.DISMISS_TOAST: { const { toastId } = action // ! Side effects ! - This could be extracted into a dismissToast() action, // but I'll keep it here for simplicity if (toastId) { if (toastTimeouts.has(toastId)) { clearTimeout(toastTimeouts.get(toastId)) toastTimeouts.delete(toastId) } } else { for (const [id, timeout] of toastTimeouts.entries()) { clearTimeout(timeout) toastTimeouts.delete(id) } } return { ...state, toasts: state.toasts.map((t) => t.id === toastId || toastId === undefined ? { ...t, open: false, } : t, ), } } case actionTypes.REMOVE_TOAST: if (action.toastId === undefined) { return { ...state, toasts: [], } } return { ...state, toasts: state.toasts.filter((t) => t.id !== action.toastId), } } } const listeners: Array<(state: State) => void> = [] let memoryState: State = { toasts: [] } function dispatch(action: Action) { memoryState = reducer(memoryState, action) listeners.forEach((listener) => { listener(memoryState) }) } type Toast = Omit function toast({ ...props }: Toast) { const id = genId() const update = (props: ToasterToast) => dispatch({ type: actionTypes.UPDATE_TOAST, toast: { ...props, id }, }) const dismiss = () => dispatch({ type: actionTypes.DISMISS_TOAST, toastId: id }) dispatch({ type: actionTypes.ADD_TOAST, toast: { ...props, id, open: true, onOpenChange: (open) => { if (!open) dismiss() }, }, }) return { id: id, dismiss, update, } } function useToast() { const [state, setState] = useState(memoryState) useEffect(() => { listeners.push(setState) return () => { const index = listeners.indexOf(setState) if (index > -1) { listeners.splice(index, 1) } } }, [state]) return { ...state, toast, dismiss: (toastId?: string) => dispatch({ type: actionTypes.DISMISS_TOAST, toastId }), } } export { useToast, toast }