import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from "react";
import { cx } from "../lib/cx";
export type ButtonVariant = "primary" | "secondary" | "fab";
export interface ButtonProps extends ButtonHTMLAttributes {
variant?: ButtonVariant;
loading?: boolean;
/** Required for `variant="fab"` — icon-only, no visible label. */
icon?: ReactNode;
}
function Spinner({ dark = false }: { dark?: boolean }) {
return (
);
}
/**
* Shared press behavior: 1px downward shift + swap from elevated to an
* inset shadow + slight darkening. This is what actually reads as
* "physically pushed in" — scale alone doesn't sell it, the inset shadow
* does the work.
*/
const pressable = cx(
"elevation-transition active:translate-y-px active:brightness-95",
"shadow-[var(--shadow-elevated)] active:shadow-[var(--shadow-pressed)]"
);
const base = cx(
"relative inline-flex items-center justify-center gap-2",
"font-body font-bold no-select [-webkit-tap-highlight-color:transparent]",
"disabled:opacity-40 disabled:pointer-events-none disabled:active:translate-y-0 disabled:active:shadow-[var(--shadow-elevated)]"
);
/**
* Touch-optimized button set. All variants meet the 48px minimum touch target.
*
* Text color rule: our brand action gradients (lime→teal, coral→amber) are
* light/high-luminance by design — see theme.css. Buttons use --ink text on
* them, not white, to stay at WCAG AA without a text-shadow crutch. The
* `secondary` variant uses --ink for the same reason (low-transparency
* surface fill, not a dark background).
*/
export const Button = forwardRef(
({ variant = "primary", loading = false, disabled, icon, className, children, ...props }, ref) => {
const isDisabled = disabled || loading;
if (variant === "fab") {
return (
);
}
if (variant === "secondary") {
return (
);
}
// primary
return (
);
}
);
Button.displayName = "Button";