"use client"; import { AlertCircle, CheckCircle2, Info, TriangleAlert, X } from "lucide-react"; import { AnimatePresence, type HTMLMotionProps, motion, useReducedMotion } from "motion/react"; import { createContext, type ComponentProps, type RefObject, useCallback, useContext, useLayoutEffect, useMemo, useRef, useState, } from "react"; import { EASE_OUT, EASE_OUT_CSS, SPRING_LAYOUT, SPRING_PRESS } from "@/lib/ease"; import { PresenceGate } from "@/lib/presence-gate"; import { cn } from "@/lib/utils"; export type AlertVariant = "default" | "info" | "success" | "warning" | "destructive"; export type AlertState = { open: boolean; variant: AlertVariant; setOpen: (open: boolean) => void; dismiss: () => void; }; const AlertContext = createContext<(AlertState & { reduce: boolean }) | null>(null); function useAlertContext(component: string) { const context = useContext(AlertContext); if (!context) throw new Error(`${component} must be used within `); return context; } /** Shared visibility and variant for custom alert children. */ export function useAlert(): AlertState { return useAlertContext("useAlert"); } const VARIANT_CLASS: Record = { default: "bg-background text-foreground", info: "bg-primary/5 text-primary", success: "bg-emerald-500/5 text-emerald-700 dark:text-emerald-400", warning: "bg-amber-500/5 text-amber-800 dark:text-amber-300", destructive: "bg-destructive/5 text-destructive", }; const VARIANT_ICON = { default: Info, info: Info, success: CheckCircle2, warning: TriangleAlert, destructive: AlertCircle, }; export interface AlertProps extends Omit, "initial" | "animate" | "exit" | "layout" | "transition" | "role"> { variant?: AlertVariant; open?: boolean; defaultOpen?: boolean; onOpenChange?: (open: boolean) => void; /** Focus this element when closing hides the currently focused alert control. */ returnFocusRef?: RefObject; /** Layout classes for the inner row, separate from the animated surface. */ contentClassName?: string; /** Defaults to alert for warning/destructive, and status for other variants. */ role?: "alert" | "status"; } export function Alert({ variant = "default", open: controlledOpen, defaultOpen = true, onOpenChange, returnFocusRef, contentClassName, className, children, role = variant === "warning" || variant === "destructive" ? "alert" : "status", style, ref, ...props }: AlertProps) { const [internalOpen, setInternalOpen] = useState(defaultOpen); const open = controlledOpen ?? internalOpen; const reduce = useReducedMotion() ?? false; const surfaceRef = useRef(null); const stateRef = useRef({ open, controlled: controlledOpen !== undefined, onOpenChange }); useLayoutEffect(() => { stateRef.current = { open, controlled: controlledOpen !== undefined, onOpenChange }; }, [controlledOpen, onOpenChange, open]); const setOpen = useCallback((next: boolean) => { const state = stateRef.current; if (next === state.open) return; if (!state.controlled) { stateRef.current = { ...state, open: next }; setInternalOpen(next); } state.onOpenChange?.(next); }, []); const dismiss = useCallback(() => setOpen(false), [setOpen]); const context = useMemo(() => ({ open, variant, reduce, setOpen, dismiss }), [open, variant, reduce, setOpen, dismiss]); const setRef = useCallback((node: HTMLDivElement | null) => { surfaceRef.current = node; const cleanup = typeof ref === "function" ? ref(node) : undefined; if (ref && typeof ref !== "function") ref.current = node; if (!node) return; return () => { surfaceRef.current = null; if (typeof cleanup === "function") cleanup(); else if (typeof ref === "function") ref(null); else if (ref) ref.current = null; }; }, [ref]); useLayoutEffect(() => { if (!open && surfaceRef.current?.contains(document.activeElement)) { returnFocusRef?.current?.focus({ preventScroll: true }); } }, [open, returnFocusRef]); return ( {open && ( {({ gate, isPresent }) => ( {children} )} )} ); } export interface AlertIconProps extends ComponentProps<"span"> {} export function AlertIcon({ className, children, ...props }: AlertIconProps) { const { variant, reduce } = useAlertContext("AlertIcon"); const Icon = VARIANT_ICON[variant]; return ( ); } export interface AlertContentProps extends ComponentProps<"div"> {} export function AlertContent({ className, ...props }: AlertContentProps) { return
; } export interface AlertTitleProps extends ComponentProps<"div"> {} export function AlertTitle({ className, ...props }: AlertTitleProps) { return
; } export interface AlertDescriptionProps extends ComponentProps<"div"> {} export function AlertDescription({ className, ...props }: AlertDescriptionProps) { return
; } export interface AlertActionProps extends ComponentProps<"div"> {} /** Compose buttons or links here; this part never creates a nested button. */ export function AlertAction({ className, ...props }: AlertActionProps) { return
; } export interface AlertCloseProps extends Omit, "layout"> {} export function AlertClose({ className, children, onClick, ...props }: AlertCloseProps) { const { dismiss, reduce } = useAlertContext("AlertClose"); return ( { onClick?.(event); if (!event.defaultPrevented) dismiss(); }} > {children ?? ); }