"use client" import * as React from "react" import { cva, type VariantProps } from "class-variance-authority" import { XIcon, CheckCircle2Icon, AlertCircleIcon, AlertTriangleIcon, InfoIcon } from "lucide-react" import { cn } from "../core/cn" const toastVariants = cva( "group/toast pointer-events-auto relative flex w-full items-start gap-3 overflow-hidden rounded-xl border p-4 pr-8 shadow-lg transition-all", { variants: { variant: { default: "bg-background text-foreground border-border", success: "bg-background border-l-4 border-l-green-500 border-border text-foreground", error: "bg-background border-l-4 border-l-destructive border-border text-foreground", warning: "bg-background border-l-4 border-l-yellow-500 border-border text-foreground", info: "bg-background border-l-4 border-l-blue-500 border-border text-foreground", }, }, defaultVariants: { variant: "default" }, } ) const variantIcons: Record = { success: , error: , warning: , info: , } interface ToastProps extends React.ComponentPropsWithoutRef<"div">, VariantProps { title?: string description?: string action?: React.ReactNode onClose?: () => void } const Toast = React.forwardRef( ({ className, variant = "default", title, description, action, onClose, children, ...props }, ref) => { return (
{variant && variant !== "default" && variantIcons[variant]}
{title && (
{title}
)} {description && (
{description}
)} {children} {action &&
{action}
}
{onClose && ( )}
) } ) Toast.displayName = "Toast" interface ToastViewportProps extends React.ComponentPropsWithoutRef<"div"> { position?: "top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right" } const positionClasses: Record, string> = { "top-left": "top-4 left-4", "top-center": "top-4 left-1/2 -translate-x-1/2", "top-right": "top-4 right-4", "bottom-left": "bottom-4 left-4", "bottom-center": "bottom-4 left-1/2 -translate-x-1/2", "bottom-right": "bottom-4 right-4", } function ToastViewport({ className, position = "bottom-right", ...props }: ToastViewportProps) { return (
) } type ToastEntry = ToastProps & { id: string duration?: number } interface ToastProviderContextValue { toasts: ToastEntry[] toast: (props: Omit) => string dismiss: (id: string) => void dismissAll: () => void } const ToastContext = React.createContext(null) function ToastProvider({ children, position = "bottom-right" }: { children: React.ReactNode; position?: ToastViewportProps["position"] }) { const [toasts, setToasts] = React.useState([]) const dismiss = React.useCallback((id: string) => { setToasts((prev) => prev.filter((t) => t.id !== id)) }, []) const dismissAll = React.useCallback(() => { setToasts([]) }, []) const toast = React.useCallback( (props: Omit) => { const id = Math.random().toString(36).slice(2) const duration = props.duration ?? 5000 setToasts((prev) => [...prev, { ...props, id }]) if (duration > 0) { setTimeout(() => dismiss(id), duration) } return id }, [dismiss] ) return ( {children} {toasts.map((t) => { const { id, duration: _duration, ...rest } = t return ( dismiss(id)} /> ) })} ) } function useToast() { const ctx = React.useContext(ToastContext) if (!ctx) throw new Error("useToast must be used within ToastProvider") return ctx } export { Toast, ToastProvider, ToastViewport, toastVariants, useToast } export type { ToastEntry, ToastProps, ToastViewportProps }