import React, { useState, useRef } from 'react'; interface CurrencyInputProps { value: number; // Value in cents (integer) onChange: (cents: number) => void; disabled?: boolean; required?: boolean; placeholder?: string; className?: string; min?: number; max?: number; } /** * ATM-style currency input where digits are entered as cents. * As more digits are entered, they shift from cents to dollars. * Only accepts integer values (digits 0-9). * * Example: typing "1234" displays "$12.34" * - Type "1" → $0.01 * - Type "2" → $0.12 * - Type "3" → $1.23 * - Type "4" → $12.34 */ const CurrencyInput: React.FC = ({ value, onChange, disabled = false, required = false, placeholder = '$0.00', className = '', min, max, }) => { const inputRef = useRef(null); const [isFocused, setIsFocused] = useState(false); // Ensure value is always an integer const safeValue = Math.floor(Math.abs(value)) || 0; // Format cents as dollars string (e.g., 1234 → "$12.34") const formatCentsAsDollars = (cents: number): string => { if (cents === 0 && !isFocused) return ''; const dollars = cents / 100; return `$${dollars.toFixed(2)}`; }; const displayValue = safeValue > 0 || isFocused ? formatCentsAsDollars(safeValue) : ''; // Process a new digit being added const addDigit = (digit: number) => { let newValue = safeValue * 10 + digit; // Enforce max if specified if (max !== undefined && newValue > max) { newValue = max; } onChange(newValue); }; // Remove the last digit const removeDigit = () => { const newValue = Math.floor(safeValue / 10); onChange(newValue); }; const handleKeyDown = (e: React.KeyboardEvent) => { // Allow navigation keys without preventing default if ( e.key === 'Tab' || e.key === 'Escape' || e.key === 'Enter' || e.key === 'ArrowLeft' || e.key === 'ArrowRight' || e.key === 'Home' || e.key === 'End' ) { return; } // Handle backspace/delete if (e.key === 'Backspace' || e.key === 'Delete') { e.preventDefault(); removeDigit(); return; } // Only allow digits 0-9 if (/^[0-9]$/.test(e.key)) { e.preventDefault(); addDigit(parseInt(e.key, 10)); return; } // Block everything else e.preventDefault(); }; // Catch input from mobile keyboards, IME, voice input, etc. const handleBeforeInput = (e: React.FormEvent) => { const inputEvent = e.nativeEvent as InputEvent; const data = inputEvent.data; // Always prevent default - we handle all input ourselves e.preventDefault(); if (!data) return; // Extract only digits from the input const digits = data.replace(/\D/g, ''); // Add each digit one at a time for (const char of digits) { addDigit(parseInt(char, 10)); } }; const handleFocus = () => { setIsFocused(true); }; const handleBlur = () => { setIsFocused(false); // Enforce min on blur if specified if (min !== undefined && safeValue < min && safeValue > 0) { onChange(min); } }; // Handle paste - extract digits only const handlePaste = (e: React.ClipboardEvent) => { e.preventDefault(); const pastedText = e.clipboardData.getData('text'); const digits = pastedText.replace(/\D/g, ''); if (digits) { let newValue = parseInt(digits, 10); if (max !== undefined && newValue > max) { newValue = max; } onChange(newValue); } }; // Handle drop - extract digits only const handleDrop = (e: React.DragEvent) => { e.preventDefault(); const droppedText = e.dataTransfer.getData('text'); const digits = droppedText.replace(/\D/g, ''); if (digits) { let newValue = parseInt(digits, 10); if (max !== undefined && newValue > max) { newValue = max; } onChange(newValue); } }; return ( {}} // Controlled via onKeyDown/onBeforeInput disabled={disabled} required={required} placeholder={placeholder} className={className} autoComplete="off" autoCorrect="off" autoCapitalize="off" spellCheck={false} /> ); }; export default CurrencyInput;