'use client' import * as React from 'react' import { Toast as BaseToast } from '@base-ui/react/toast' import { type VariantProps } from 'class-variance-authority' import { cn } from '../../internal/utils' import { useDirection } from '../../hooks/use-direction' import { buttonVariants } from '../button/button-variants' type ToastPosition = 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right' type ToastSwipeAxis = 'up' | 'down' | 'left' | 'right' const viewportPositionClasses: Record = { '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', } const swipeDirectionByPosition: Record = { 'top-left': ['up', 'left'], 'top-center': ['up'], 'top-right': ['up', 'right'], 'bottom-left': ['down', 'left'], 'bottom-center': ['down'], 'bottom-right': ['down', 'right'], } const toastAnimationBase = '[--gap:0.75rem] [--peek:0.75rem] ' + '[--scale:calc(max(0,1-(var(--toast-index)*0.1)))] ' + '[--shrink:calc(1-var(--scale))] ' + '[--height:var(--toast-frontmost-height,var(--toast-height))] ' + 'absolute z-[calc(1000-var(--toast-index))] w-full select-none ' + 'h-[var(--height)] data-expanded:h-[var(--toast-height)] ' + 'data-ending-style:opacity-0 data-limited:opacity-0 ' + '[transition:transform_0.5s_cubic-bezier(0.22,1,0.36,1),opacity_0.5s,height_0.15s] ' + 'motion-reduce:transition-none' const toastAnimationBottom = 'right-0 bottom-0 left-0 origin-bottom ' + '[--offset-y:calc(var(--toast-offset-y)*-1+calc(var(--toast-index)*var(--gap)*-1)+var(--toast-swipe-movement-y))] ' + '[transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)-(var(--toast-index)*var(--peek))-(var(--shrink)*var(--height))))_scale(var(--scale))] ' + 'data-expanded:[transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--offset-y)))] ' + 'motion-safe:data-starting-style:[transform:translateY(150%)] ' + 'motion-safe:[&[data-ending-style]:not([data-limited]):not([data-swipe-direction])]:[transform:translateY(150%)] ' + 'motion-safe:data-ending-style:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))] ' + 'motion-safe:data-expanded:data-ending-style:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))] ' + 'motion-safe:data-ending-style:data-[swipe-direction=left]:[transform:translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))] ' + 'motion-safe:data-expanded:data-ending-style:data-[swipe-direction=left]:[transform:translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))] ' + 'motion-safe:data-ending-style:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))] ' + 'motion-safe:data-expanded:data-ending-style:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))] ' + 'motion-safe:data-ending-style:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))] ' + 'motion-safe:data-expanded:data-ending-style:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))] ' + "after:absolute after:top-full after:left-0 after:h-[calc(var(--gap)+1px)] after:w-full after:content-['']" const toastAnimationTop = 'top-0 right-0 left-0 origin-top ' + '[--offset-y:calc(var(--toast-offset-y)+(var(--toast-index)*var(--gap))+var(--toast-swipe-movement-y))] ' + '[transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--toast-swipe-movement-y)+(var(--toast-index)*var(--peek))+(var(--shrink)*var(--height))))_scale(var(--scale))] ' + 'data-expanded:[transform:translateX(var(--toast-swipe-movement-x))_translateY(calc(var(--offset-y)))] ' + 'motion-safe:data-starting-style:[transform:translateY(-150%)] ' + 'motion-safe:[&[data-ending-style]:not([data-limited]):not([data-swipe-direction])]:[transform:translateY(-150%)] ' + 'motion-safe:data-ending-style:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))] ' + 'motion-safe:data-expanded:data-ending-style:data-[swipe-direction=down]:[transform:translateY(calc(var(--toast-swipe-movement-y)+150%))] ' + 'motion-safe:data-ending-style:data-[swipe-direction=left]:[transform:translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))] ' + 'motion-safe:data-expanded:data-ending-style:data-[swipe-direction=left]:[transform:translateX(calc(var(--toast-swipe-movement-x)-150%))_translateY(var(--offset-y))] ' + 'motion-safe:data-ending-style:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))] ' + 'motion-safe:data-expanded:data-ending-style:data-[swipe-direction=right]:[transform:translateX(calc(var(--toast-swipe-movement-x)+150%))_translateY(var(--offset-y))] ' + 'motion-safe:data-ending-style:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))] ' + 'motion-safe:data-expanded:data-ending-style:data-[swipe-direction=up]:[transform:translateY(calc(var(--toast-swipe-movement-y)-150%))] ' + "after:absolute after:bottom-full after:left-0 after:h-[calc(var(--gap)+1px)] after:w-full after:content-['']" const ToastPositionContext = React.createContext('bottom-right') interface ToastProviderProps extends BaseToast.Provider.Props {} function ToastProvider(props: ToastProviderProps) { return } interface ToasterProps extends Omit { /** * Where the stack is anchored; also sets the default swipe-to-dismiss directions. * @default 'bottom-right' */ position?: ToastPosition /** * Opt in to a countdown progress bar on auto-dismissing toasts. * @default false */ progress?: boolean /** * Provider default (ms) used to size the progress bar for toasts that don't set their own `timeout`. Match your * `ToastProvider`'s `timeout`. * @default 5000 */ timeout?: number /** * Portal target - scope the toasts to a specific element. * @default document.body */ container?: React.ComponentProps['container'] /** Escape hatch forwarded to the underlying `Toast.Portal`. */ portalProps?: Omit, 'children'> } function Toaster({ position = 'bottom-right', progress = false, timeout = 5000, className, container, portalProps, ...viewportProps }: ToasterProps) { const { toasts } = BaseToast.useToastManager<{ icon?: React.ReactNode }>() const portal = { ...portalProps, ...(container !== undefined ? { container } : {}) } return ( {toasts.map((toast) => ( {toast.data?.icon ? {toast.data.icon} : null} {toast.title ? {toast.title} : null} {toast.description ? {toast.description} : null} {toast.actionProps ? ( ) : null} ))} ) } interface ToastViewportProps extends BaseToast.Viewport.Props { position?: ToastPosition } function ToastViewport({ position: positionProp, className, ...props }: ToastViewportProps) { const ctxPosition = React.useContext(ToastPositionContext) const position = positionProp ?? ctxPosition const direction = useDirection() return ( ) } interface ToastProps extends Omit { position?: ToastPosition dismissible?: boolean progress?: boolean providerTimeout?: number closeLabel?: string swipeDirection?: BaseToast.Root.Props['swipeDirection'] } function Toast({ position: positionProp, dismissible = true, progress = false, providerTimeout = 5000, closeLabel, swipeDirection, toast, className, children, ...props }: ToastProps) { const ctxPosition = React.useContext(ToastPositionContext) const position = positionProp ?? ctxPosition const resolvedTimeout = toast.timeout ?? providerTimeout const showProgress = progress && resolvedTimeout > 0 const isBottom = position.startsWith('bottom') return ( {children} {dismissible ? closeLabel !== undefined ? : : null} {showProgress ? : null} ) } interface ToastIconProps extends React.HTMLAttributes {} function ToastIcon({ className, ...props }: ToastIconProps) { return ( svg]:h-lh [&>svg:not([class*='size-'])]:size-5", className, )} {...props} /> ) } interface ToastTitleProps extends BaseToast.Title.Props {} function ToastTitle({ className, ...props }: ToastTitleProps) { return ( ) } interface ToastDescriptionProps extends BaseToast.Description.Props {} function ToastDescription({ className, ...props }: ToastDescriptionProps) { return ( ) } interface ToastActionsProps extends React.HTMLAttributes {} function ToastActions({ className, ...props }: ToastActionsProps) { return (
) } interface ToastActionProps extends BaseToast.Action.Props, VariantProps {} function ToastAction({ className, variant = 'primary', size = 'sm', ...props }: ToastActionProps) { return ( ) } interface ToastCloseProps extends BaseToast.Close.Props { closeLabel?: string } function ToastClose({ className, closeLabel = 'Dismiss', children, ...props }: ToastCloseProps) { return ( {children ?? } ) } function CloseIcon() { return ( ) } interface ToastProgressProps extends Omit, 'style'> { timeout: number } function ToastProgress({ timeout, className, ...props }: ToastProgressProps) { return (
) } type ToastPortalProps = React.ComponentProps function ToastPortal(props: ToastPortalProps) { return } const useToastManager = BaseToast.useToastManager const createToastManager = BaseToast.createToastManager export { ToastProvider, Toaster, ToastViewport, ToastPortal, Toast, ToastIcon, ToastTitle, ToastDescription, ToastActions, ToastAction, ToastClose, ToastProgress, useToastManager, createToastManager, } export type { ToastProviderProps, ToasterProps, ToastViewportProps, ToastPortalProps, ToastProps, ToastIconProps, ToastTitleProps, ToastDescriptionProps, ToastActionsProps, ToastActionProps, ToastCloseProps, ToastProgressProps, ToastPosition, }