Files
cadence-ui/packages/ui/src/components/button.tsx
T

121 lines
3.2 KiB
TypeScript

import { Slot, Slottable } from "@radix-ui/react-slot";
import { forwardRef, useState } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { buttonVariants } from "./button.variants";
import { cn } from "../lib/cn";
import type { VariantProps } from "../lib/cva";
import type { ButtonLikeElementProps } from "../lib/contracts";
import { createDataAttributes, createSlot } from "../lib/contracts";
export type ButtonProps = Omit<
ButtonLikeElementProps,
"onDrag" | "onDragEnd" | "onDragStart"
> &
VariantProps<typeof buttonVariants>;
function Spinner() {
return (
<motion.span
{...createSlot("icon")}
aria-hidden="true"
animate={{ opacity: 1, rotate: 0, scale: 1 }}
className="size-4 rounded-full border-2 border-current border-r-transparent animate-spin"
exit={{ opacity: 0, rotate: 90, scale: 0.7 }}
initial={{ opacity: 0, rotate: -90, scale: 0.7 }}
transition={{
duration: 0.18,
ease: [0.22, 1, 0.36, 1]
}}
/>
);
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
{
asChild = false,
children,
className,
disabled,
loading = false,
onMouseEnter,
onMouseLeave,
size,
type,
variant,
...props
},
ref
) {
const prefersReducedMotion = useReducedMotion();
const [isHovered, setIsHovered] = useState(false);
const isDisabled = disabled || loading;
const Component = asChild ? Slot : "button";
const baseClassName = cn(buttonVariants({ loading, size, variant }), className);
const label = asChild ? (
<Slottable>{children}</Slottable>
) : (
<motion.span
{...createSlot("label")}
animate={{
opacity: loading ? 0.94 : 1,
x: loading ? 1.5 : 0
}}
transition={{
duration: prefersReducedMotion ? 0.01 : 0.18,
ease: [0.22, 1, 0.36, 1]
}}
>
{children}
</motion.span>
);
const sheen = !asChild ? (
<motion.span
animate={
prefersReducedMotion || isDisabled
? { opacity: 0, x: "-120%" }
: isHovered
? { opacity: 0.75, x: "115%" }
: { opacity: 0, x: "-120%" }
}
aria-hidden="true"
className="pointer-events-none absolute inset-y-0 left-0 w-1/2 rounded-[inherit] bg-[linear-gradient(120deg,transparent_0%,rgba(255,255,255,0.32)_45%,transparent_100%)] mix-blend-screen"
initial={false}
transition={{
duration: prefersReducedMotion ? 0.01 : 0.55,
ease: [0.16, 1, 0.3, 1]
}}
/>
) : null;
return (
<Component
{...props}
{...createSlot("root")}
{...createDataAttributes({
disabled: isDisabled,
loading,
size,
variant
})}
className={baseClassName}
disabled={asChild ? undefined : isDisabled}
onMouseEnter={(event) => {
setIsHovered(true);
onMouseEnter?.(event as never);
}}
onMouseLeave={(event) => {
setIsHovered(false);
onMouseLeave?.(event as never);
}}
ref={ref}
type={asChild ? undefined : type ?? "button"}
>
{sheen}
<AnimatePresence initial={false}>{loading ? <Spinner /> : null}</AnimatePresence>
{label}
</Component>
);
});