| 107 | } |
| 108 | |
| 109 | function ToastItem({ id, title, description, variant = 'default', duration = 5000 }: ToastItemProps) { |
| 110 | const [open, setOpen] = React.useState(true) |
| 111 | const icon = variantIcons[variant ?? 'default'] |
| 112 | |
| 113 | return ( |
| 114 | <RadixToast.Root |
| 115 | open={open} |
| 116 | onOpenChange={(o) => { |
| 117 | setOpen(o) |
| 118 | if (!o) setTimeout(() => toast.dismiss(id), 300) |
| 119 | }} |
| 120 | duration={duration} |
| 121 | className={cn(toastVariants({ variant }))} |
| 122 | > |
| 123 | {icon} |
| 124 | <div className="flex-1 min-w-0"> |
| 125 | <RadixToast.Title className="text-sm font-medium leading-snug">{title}</RadixToast.Title> |
| 126 | {description && ( |
| 127 | <RadixToast.Description className="mt-0.5 text-xs text-surface-400 leading-relaxed"> |
| 128 | {description} |
| 129 | </RadixToast.Description> |
| 130 | )} |
| 131 | </div> |
| 132 | <RadixToast.Close |
| 133 | className="flex-shrink-0 text-surface-500 hover:text-surface-200 transition-colors rounded focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" |
| 134 | aria-label="Dismiss notification" |
| 135 | > |
| 136 | <X className="h-4 w-4" aria-hidden="true" /> |
| 137 | </RadixToast.Close> |
| 138 | {/* Progress bar */} |
| 139 | <div |
| 140 | className="absolute bottom-0 left-0 h-0.5 w-full origin-left bg-current opacity-20" |
| 141 | style={{ animation: `progress ${duration}ms linear forwards` }} |
| 142 | aria-hidden="true" |
| 143 | /> |
| 144 | </RadixToast.Root> |
| 145 | ) |
| 146 | } |
| 147 | |
| 148 | // ── Provider (mount once in layout) ────────────────────────────────────────── |
| 149 | |