import { forwardRef } from "react"; import { Button, ButtonProps, CircularProgress } from "@mui/material"; import { alpha, keyframes, styled } from "@mui/material/styles"; import type { Theme } from "@mui/material/styles"; type AnimatedCircleButtonProps = ButtonProps & { disableAnimation?: boolean; loading?: boolean; }; type StyledButtonProps = AnimatedCircleButtonProps & { theme: Theme }; const loadingPulse = keyframes` 0% { transform: translate(-50%, -50%) scale(0.6); opacity: 0.35; } 50% { transform: translate(-50%, -50%) scale(1.45); opacity: 0.15; } 100% { transform: translate(-50%, -50%) scale(0.6); opacity: 0; } `; const StyledButton = styled(Button, { shouldForwardProp: (prop) => prop !== "disableAnimation" && prop !== "loading", })((props: StyledButtonProps) => { const { theme, disableAnimation = false, color, variant = "text", disabled = false, loading = false, } = props; const shouldAnimate = !disableAnimation && (!disabled || loading); const pointerBlocked = loading; const paletteMainMap: Record = { primary: theme.palette.primary.main, secondary: theme.palette.secondary.main, error: theme.palette.error.main, warning: theme.palette.warning.main, info: theme.palette.info.main, success: theme.palette.success.main, inherit: theme.palette.primary.main, }; const paletteMain = (color && paletteMainMap[String(color)]) ?? theme.palette.primary.main; const pulseColor = variant === "outlined" || variant === "text" ? alpha(paletteMain, 0.18) : alpha(paletteMain, 0.3); return { position: "relative", overflow: "hidden", borderRadius: 5, zIndex: 0, transition: "transform 0.2s ease, box-shadow 0.2s ease", pointerEvents: pointerBlocked ? "none" : undefined, "&::after": shouldAnimate ? { content: '""', position: "absolute", width: "12px", height: "12px", backgroundColor: pulseColor, borderRadius: "50%", top: "50%", left: "50%", pointerEvents: "none", zIndex: 0, ...(loading ? { opacity: 0.35, transform: "translate(-50%, -50%) scale(0.6)", animation: `${loadingPulse} 1.2s ease-in-out infinite`, } : { opacity: 0, transform: "translate(-50%, -50%) scale(0)", transition: "transform 0.45s ease, opacity 0.45s ease", }), } : {}, ...(loading ? {} : { "&:hover": { transform: "translateY(-1px)", boxShadow: theme.shadows[4], }, "&:hover::after": shouldAnimate ? { transform: "translate(-50%, -50%) scale(15)", opacity: 1, } : {}, "&:active": { transform: "translateY(0)", boxShadow: theme.shadows[2], }, "&:active::after": shouldAnimate ? { transform: "translate(-50%, -50%) scale(18)", opacity: 0.4, } : {}, }), "&.Mui-disabled": { boxShadow: "none", transform: "none", ...(loading && shouldAnimate ? {} : { "&::after": { opacity: 0, }, }), }, ...(disabled && { boxShadow: "none", transform: "none", }), "& > *": { position: "relative", zIndex: 1, }, }; }); export const AnimatedCircleButton = forwardRef< HTMLButtonElement, AnimatedCircleButtonProps >((props, ref) => { const { loading = false, disabled, children, startIcon, endIcon, ...rest } = props; const effectiveStartIcon = loading ? ( ) : ( startIcon ); return ( {children} ); });