lightswind
Version:
A collection of beautifully crafted React Components, Blocks & Templates for Modern Developers. Create stunning web applications effortlessly by using our 160+ professional and animated react components.
68 lines (67 loc) • 3.81 kB
JavaScript
// @ts-nocheck
"use client";
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Minus, Plus } from "lucide-react";
import { cn } from "../../lib/utils";
export function AnimatedNumberStepper({ defaultValue = 1, min = 0, max = 99, step = 1, onChange, size = "md", label = "Quantity", className, }) {
const [value, setValue] = useState(defaultValue);
const [direction, setDirection] = useState("up");
const sizes = {
sm: {
container: "h-9 gap-1",
button: "h-9 w-9 text-sm",
display: "w-12 h-9 text-sm",
},
md: {
container: "h-11 gap-1.5",
button: "h-11 w-11",
display: "w-16 h-11 text-base",
},
lg: {
container: "h-14 gap-2",
button: "h-14 w-14 text-lg",
display: "w-20 h-14 text-lg",
},
};
const iconSizes = {
sm: "h-3.5 w-3.5",
md: "h-4 w-4",
lg: "h-5 w-5",
};
const s = sizes[size];
const handleDecrement = () => {
if (value - step < min)
return;
setDirection("down");
const next = Math.max(min, value - step);
setValue(next);
onChange?.(next);
};
const handleIncrement = () => {
if (value + step > max)
return;
setDirection("up");
const next = Math.min(max, value + step);
setValue(next);
onChange?.(next);
};
const isAtMin = value <= min;
const isAtMax = value >= max;
const variants = {
enter: (dir) => ({
y: dir === "up" ? 16 : -16,
opacity: 0,
}),
center: { y: 0, opacity: 1 },
exit: (dir) => ({
y: dir === "up" ? -16 : 16,
opacity: 0,
}),
};
return (_jsxs("div", { className: cn("inline-flex items-center", s.container, className), "aria-label": label, children: [_jsx(motion.button, { type: "button", onClick: handleDecrement, disabled: isAtMin, "aria-label": "Decrement", className: cn("relative flex items-center justify-center rounded-xl border bg-background shadow-sm transition-colors", "hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", "disabled:opacity-30 disabled:cursor-not-allowed", s.button), whileTap: { scale: isAtMin ? 1 : 0.88 }, whileHover: { scale: isAtMin ? 1 : 1.05 }, transition: { type: "spring", stiffness: 400, damping: 20 }, children: _jsx(Minus, { className: iconSizes[size] }) }), _jsx("div", { className: cn("relative flex items-center justify-center overflow-hidden rounded-xl border bg-muted/30 font-semibold tabular-nums select-none", s.display), children: _jsx(AnimatePresence, { mode: "popLayout", custom: direction, initial: false, children: _jsx(motion.span, { custom: direction, variants: variants, initial: "enter", animate: "center", exit: "exit", transition: {
y: { type: "spring", stiffness: 500, damping: 35 },
opacity: { duration: 0.12 },
}, className: "absolute", children: value }, value) }) }), _jsx(motion.button, { type: "button", onClick: handleIncrement, disabled: isAtMax, "aria-label": "Increment", className: cn("relative flex items-center justify-center rounded-xl border bg-background shadow-sm transition-colors", "hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", "disabled:opacity-30 disabled:cursor-not-allowed", s.button), whileTap: { scale: isAtMax ? 1 : 0.88 }, whileHover: { scale: isAtMax ? 1 : 1.05 }, transition: { type: "spring", stiffness: 400, damping: 20 }, children: _jsx(Plus, { className: iconSizes[size] }) })] }));
}