"use client"; import { AlertTriangle, Check, Circle, Info, LoaderCircle, X, type LucideIcon, } from "lucide-react"; import { AnimatePresence, motion, useReducedMotion, type HTMLMotionProps, type Variants, } from "motion/react"; import type { ReactNode } from "react"; import { EASE_OUT } from "@/lib/ease"; import { cn } from "@/lib/utils"; export type AnimatedBadgeStatus = | "neutral" | "info" | "success" | "warning" | "danger" | "loading"; export type AnimatedBadgeSize = "sm" | "md"; export interface AnimatedBadgeProps extends Omit< HTMLMotionProps<"span">, "children" > { status?: AnimatedBadgeStatus; size?: AnimatedBadgeSize; children?: ReactNode; icon?: ReactNode; showIcon?: boolean; pulse?: boolean; contentKey?: string | number; } const STATUS_CLASS: Record = { neutral: "border-border bg-card text-muted-foreground", info: "border-primary/30 bg-primary/10 text-primary", success: "border-emerald-500/30 bg-emerald-500/10 text-emerald-600 dark:text-emerald-400", warning: "border-amber-500/30 bg-amber-500/10 text-amber-600 dark:text-amber-400", danger: "border-destructive/30 bg-destructive/10 text-destructive", loading: "border-primary/30 bg-primary/10 text-primary", }; const SIZE_CLASS: Record = { sm: "h-6 gap-1.5 px-2 text-[11px]", md: "h-8 gap-2 px-3 text-xs", }; const ICON_CLASS: Record = { sm: "h-3 w-3", md: "h-3.5 w-3.5", }; const ICONS: Record = { neutral: Circle, info: Info, success: Check, warning: AlertTriangle, danger: X, loading: LoaderCircle, }; const ICON_ROLL_VARIANTS: Variants = { initial: { opacity: 0.72, y: "80%", scale: 0.92, rotate: -8, filter: "blur(6px)", }, animate: { opacity: 1, y: "0%", scale: 1, rotate: 0, filter: "blur(0px)", transition: { y: { type: "spring", stiffness: 210, damping: 24, mass: 0.85 }, scale: { type: "spring", stiffness: 250, damping: 24, mass: 0.75 }, rotate: { duration: 0.28, ease: EASE_OUT }, opacity: { duration: 0.28, ease: EASE_OUT }, filter: { duration: 0.42, ease: EASE_OUT }, }, }, exit: { opacity: 0.5, y: "-80%", scale: 0.96, rotate: 8, filter: "blur(6px)", transition: { duration: 0.22, ease: EASE_OUT }, }, }; const TEXT_ROLL_VARIANTS: Variants = { initial: { opacity: 0.76, y: "85%", filter: "blur(6px)" }, animate: { opacity: 1, y: "0%", filter: "blur(0px)", transition: { y: { type: "spring", stiffness: 210, damping: 24, mass: 0.85 }, opacity: { duration: 0.3, ease: EASE_OUT }, filter: { duration: 0.42, ease: EASE_OUT }, }, }, exit: { opacity: 0.5, y: "-85%", filter: "blur(6px)", transition: { duration: 0.2, ease: EASE_OUT }, }, }; export function AnimatedBadge({ status = "neutral", size = "md", children, icon, showIcon = true, pulse = status === "loading", contentKey, className, ...rest }: AnimatedBadgeProps) { const reduce = useReducedMotion(); const Icon = ICONS[status]; const resolvedContentKey = contentKey ?? (typeof children === "string" || typeof children === "number" ? children : status); return ( {pulse && !reduce ? ( ) : null} {showIcon ? ( {status === "loading" && !reduce && !icon ? ( ) : ( (icon ?? ) )} ) : null} {children != null ? ( {children} ) : null} ); }