"use client"; import { useReducedMotion } from "../motion/Preferences.tsx"; import { createContext, useCallback, useContext, useEffect, useRef, useState, type ReactNode, } from "react"; import { AnimatePresence, motion } from "motion/react"; import { EASE_EXPO } from "../motion/easings.ts"; import { Mark } from "../ui/Mark.tsx"; export type ToastTone = "default" | "success" | "error"; type ToastItem = { id: number; message: string; tone: ToastTone; }; type ToastContextValue = { toast: ( message: string, options?: { tone?: ToastTone; duration?: number }, ) => void; }; const ToastContext = createContext(null); export function useToast(): ToastContextValue { const ctx = useContext(ToastContext); if (!ctx) throw new Error("useToast must be used within "); return ctx; } const TONE_MARK: Record = { default: "paper", success: "accent", error: "accent", }; export function ToastProvider({ children }: { children: ReactNode }) { const [toasts, setToasts] = useState([]); const idRef = useRef(0); const timers = useRef(new Map()); useEffect( () => () => { timers.current.forEach(window.clearTimeout); timers.current.clear(); }, [], ); const reduce = useReducedMotion(); const dismiss = useCallback((id: number) => { window.clearTimeout(timers.current.get(id)); timers.current.delete(id); setToasts((ts) => ts.filter((t) => t.id !== id)); }, []); const toast = useCallback( (message: string, options?: { tone?: ToastTone; duration?: number }) => { const id = ++idRef.current; const item: ToastItem = { id, message, tone: options?.tone ?? "default" }; setToasts((ts) => [...ts.slice(-3), item]); const duration = options?.duration ?? 3500; if (duration > 0) timers.current.set( id, window.setTimeout(() => dismiss(id), duration), ); }, [dismiss], ); return ( {children}
{toasts.map((t) => ( {t.message} {t.tone === "success" && ( )} {t.tone === "error" && ( )} ))}
); }