"use client";
import * as React from "react";
import { Minus, Plus } from "lucide-react";
import { cn } from "@/lib/utils";
export interface QuantityStepperProps {
value?: number;
defaultValue?: number;
min?: number;
max?: number;
onChange?: (value: number) => void;
size?: "sm" | "md";
className?: string;
label?: string;
}
export function QuantityStepper({
value,
defaultValue = 1,
min = 1,
max = 99,
onChange,
size = "md",
className,
label = "Quantity",
}: QuantityStepperProps) {
const [inner, setInner] = React.useState(defaultValue);
const current = value ?? inner;
const [dir, setDir] = React.useState<1 | -1>(1);
const set = (n: number) => {
const next = Math.min(max, Math.max(min, n));
if (next === current) return;
setDir(next > current ? 1 : -1);
if (value === undefined) setInner(next);
onChange?.(next);
};
const btn = cn(
"grid place-items-center rounded-full text-foreground transition hover:bg-accent disabled:pointer-events-none disabled:opacity-35",
size === "sm" ? "size-7" : "size-9",
);
return (
<div
role="group"
aria-label={label}
className={cn("inline-flex items-center gap-1 rounded-full border bg-background p-1 shadow-xs", className)}
>
<button type="button" className={btn} onClick={() => set(current - 1)} disabled={current <= min} aria-label="Decrease">
<Minus className="size-4" />
</button>
<span
role="spinbutton"
tabIndex={0}
aria-valuenow={current}
aria-valuemin={min}
aria-valuemax={max}
onKeyDown={(e) => {
if (e.key === "ArrowUp" || e.key === "ArrowRight") (e.preventDefault(), set(current + 1));
if (e.key === "ArrowDown" || e.key === "ArrowLeft") (e.preventDefault(), set(current - 1));
}}
className={cn("relative overflow-hidden text-center font-medium tabular-nums outline-none focus-visible:ring-2 focus-visible:ring-ring/40 rounded", size === "sm" ? "w-7 text-sm" : "w-9")}
>
<span key={current} className={cn("block", dir === 1 ? "animate-in slide-in-from-bottom-2" : "animate-in slide-in-from-top-2", "fade-in duration-200")}>
{current}
</span>
</span>
<button type="button" className={btn} onClick={() => set(current + 1)} disabled={current >= max} aria-label="Increase">
<Plus className="size-4" />
</button>
</div>
);
}