diff --git a/client/src/components/ToastProvider.jsx b/client/src/components/ToastProvider.jsx
new file mode 100644
index 0000000..193ff47
--- /dev/null
+++ b/client/src/components/ToastProvider.jsx
@@ -0,0 +1,145 @@
+import React, { createContext, useContext, useState, useCallback, useRef, useEffect } from 'react';
+
+const ToastContext = createContext(null);
+
+export function useToast() {
+ const ctx = useContext(ToastContext);
+ if (!ctx) throw new Error('useToast must be used within a ToastProvider');
+ return ctx;
+}
+
+const VARIANTS = {
+ success: { bg: '#053321', border: '#0f5132', color: '#9ef7c1', icon: '✓' },
+ error: { bg: '#3c1114', border: '#f5c6cb', color: '#ffb3b8', icon: '✗' },
+ info: { bg: '#0c1f3f', border: '#2563eb', color: '#93c5fd', icon: 'ℹ' },
+ warning: { bg: '#3b2e00', border: '#d4af37', color: '#ffdf8a', icon: '⚠' },
+};
+
+let nextId = 0;
+
+function Toast({ toast, onDismiss }) {
+ const v = VARIANTS[toast.variant] || VARIANTS.info;
+ const [exiting, setExiting] = useState(false);
+ const timerRef = useRef(null);
+
+ useEffect(() => {
+ timerRef.current = setTimeout(() => {
+ setExiting(true);
+ setTimeout(() => onDismiss(toast.id), 280);
+ }, toast.duration || 4000);
+ return () => clearTimeout(timerRef.current);
+ }, [toast.id, toast.duration, onDismiss]);
+
+ const handleDismiss = () => {
+ clearTimeout(timerRef.current);
+ setExiting(true);
+ setTimeout(() => onDismiss(toast.id), 280);
+ };
+
+ return (
+
+
{v.icon}
+
{toast.message}
+
+
+
+ );
+}
+
+export default function ToastProvider({ children }) {
+ const [toasts, setToasts] = useState([]);
+
+ const dismiss = useCallback((id) => {
+ setToasts(prev => prev.filter(t => t.id !== id));
+ }, []);
+
+ const addToast = useCallback((message, variant = 'info', duration = 4000) => {
+ const id = ++nextId;
+ setToasts(prev => {
+ const next = [...prev, { id, message, variant, duration }];
+ return next.length > 5 ? next.slice(-5) : next;
+ });
+ return id;
+ }, []);
+
+ const toast = useCallback({
+ success: (msg, dur) => addToast(msg, 'success', dur),
+ error: (msg, dur) => addToast(msg, 'error', dur || 6000),
+ info: (msg, dur) => addToast(msg, 'info', dur),
+ warning: (msg, dur) => addToast(msg, 'warning', dur || 5000),
+ }, [addToast]);
+
+ // Inject keyframes once
+ useEffect(() => {
+ if (document.getElementById('toast-keyframes')) return;
+ const style = document.createElement('style');
+ style.id = 'toast-keyframes';
+ style.textContent = `
+ @keyframes toastIn {
+ from { opacity: 0; transform: translateX(100%); }
+ to { opacity: 1; transform: translateX(0); }
+ }
+ @keyframes toastOut {
+ from { opacity: 1; transform: translateX(0); }
+ to { opacity: 0; transform: translateX(100%); }
+ }
+ @keyframes toastProgress {
+ from { width: 100%; }
+ to { width: 0%; }
+ }
+ `;
+ document.head.appendChild(style);
+ }, []);
+
+ return (
+
+ {children}
+
+ {toasts.map(t => (
+
+
+
+ ))}
+
+
+ );
+}