Fazekit

Code

"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>
  );
}

More in E-commerce

View all →