"use client"; import { cn } from "@/utils/ui"; import { useRef, useState, type ComponentProps } from "react"; import { useFocusLock } from "@/hooks/use-focus-lock"; import { Button } from "@/components/ui/button"; import { HugeiconsIcon } from "@hugeicons/react"; import { ArrowTurnBackwardIcon } from "@hugeicons/core-free-icons"; const DRAG_SENSITIVITIES = { default: 1, slow: 0.5, } as const; type DragSensitivity = "default" | "slow"; interface NumberFieldProps extends Omit, "size" | "type"> { icon?: React.ReactNode; dragSensitivity?: DragSensitivity; onScrub?: (value: number) => void; onScrubEnd?: () => void; allowExpressions?: boolean; onReset?: () => void; isDefault?: boolean; } function NumberField({ className, icon, disabled, dragSensitivity = "default", onScrub, onScrubEnd, value, allowExpressions = true, onKeyDown, onFocus, onBlur, onMouseDown, onReset, isDefault = false, ref, ...props }: NumberFieldProps & { ref?: React.Ref }) { const iconRef = useRef(null); const inputRef = useRef(null); const startValueRef = useRef(0); const cumulativeDeltaRef = useRef(0); const [isInputFocused, setIsInputFocused] = useState(false); const { containerRef: wrapperRef } = useFocusLock({ isActive: isInputFocused, onDismiss: () => inputRef.current?.blur(), cursor: "text", allowSelector: "input, textarea, [contenteditable]", }); const handleIconPointerDown = (event: React.PointerEvent) => { if (!onScrub || disabled || event.button !== 0) return; const parsed = parseFloat(String(value ?? "0")); startValueRef.current = Number.isNaN(parsed) ? 0 : parsed; cumulativeDeltaRef.current = 0; let hasReceivedFirstMove = false; iconRef.current?.requestPointerLock(); const handlePointerMove = (moveEvent: PointerEvent) => { // first movementX after pointer lock often contains a bogus warp delta if (!hasReceivedFirstMove) { hasReceivedFirstMove = true; return; } cumulativeDeltaRef.current += moveEvent.movementX; const newValue = startValueRef.current + cumulativeDeltaRef.current * DRAG_SENSITIVITIES[dragSensitivity]; onScrub(newValue); }; const handlePointerUp = () => { document.removeEventListener("pointermove", handlePointerMove); document.removeEventListener("pointerup", handlePointerUp); document.exitPointerLock(); onScrubEnd?.(); }; document.addEventListener("pointermove", handlePointerMove); document.addEventListener("pointerup", handlePointerUp); }; const canScrub = Boolean(icon && onScrub); return (
{icon && ( {icon} )} { const inputElement = event.currentTarget; const shouldPreventNativeCaretPlacement = event.button === 0 && document.activeElement !== inputElement; if (shouldPreventNativeCaretPlacement) { event.preventDefault(); inputElement.focus(); inputElement.select(); } onMouseDown?.(event); }} onFocus={(event) => { setIsInputFocused(true); event.currentTarget.select(); onFocus?.(event); }} onKeyDown={(event) => { const shouldBlurInput = event.key === "Enter" || event.key === "Escape"; if (shouldBlurInput) event.currentTarget.blur(); onKeyDown?.(event); }} onBlur={(event) => { setIsInputFocused(false); onBlur?.(event); }} {...props} /> {onReset && !isDefault && (
)}
); } export { NumberField };