Repository: motion-canvas/motion-canvas
File: packages/ui/src/components/controls/NumberInput.tsx (TypeScript)

Code before:
import {MouseButton, clamp} from '../../utils';
import styles from './Controls.module.scss';

type NumberInputProps = Omit<
  JSX.HTMLAttributes<HTMLInputElement>,
  'min' | 'max' | 'step' | 'value' | 'onChange' | 'onChangeCapture'
> & {
  min?: number;
  max?: number;
  step?: number;
  value: number;
  onChange?: (value: number) => void;
  onChangeCapture?: (value: number) => void;
};

export function NumberInput({
  min = -Infinity,
  max = Infinity,
  step = 1,
  value = 0,
  onChange,
  onChangeCapture,
  ...props
}: NumberInputProps) {
  const handleChange = onChangeCapture ? onChange : undefined;
  const handleChangeCapture = onChangeCapture ?? onChange;

  const inputRef = useRef<HTMLInputElement>();
  const [currentValue, setCurrentValue] = useState(value);
  const [active, setActive] = useState(false);
  const [startX, setStartX] = useState(0);

  useEffect(() => {
    setCurrentValue(value);
  }, [value]);

  const handleDrag = useCallback(
    (event: MouseEvent) => {
      setCurrentValue(value => {
        // ignore very large jumps in movement, as they often result from
        // mouse acceleration issues
        if (Math.abs(event.movementX) > 100) {
          return value;
        }

        const newValue = clamp(min, max, value + event.movementX * step);
        handleChange?.(newValue);
        return newValue;
      });
    },
    [min, max, step],

Review comment (suggestion):
I don't think this is necessary here.
The `Input` component corrects the discrepancy between Preact's `onChange` and the browser's `onchange` by using `onChangeCapture` and since it's a generic component I left the possibility to not use the correction.
This is a pretty concrete component that has its own custom `onChange` callback so I think we can just dispatch it during `onChangeCapture` as that's how it's intended to work.

Code after:
import {MouseButton, clamp} from '../../utils';
import styles from './Controls.module.scss';

type NumberInputProps = Omit<
  JSX.HTMLAttributes<HTMLInputElement>,
  'value' | 'onChange' | 'min' | 'max' | 'step'
> & {
  value: number;
  onChange: (value: number) => void;
  min?: number;
  max?: number;
  step?: number;
};

export function NumberInput({
  value,
  onChange,
  min = -Infinity,
  max = Infinity,
  step = 1,
  ...props
}: NumberInputProps) {
  const inputRef = useRef<HTMLInputElement>();
  const [currentValue, setCurrentValue] = useState(value);
  const [startX, setStartX] = useState(0);

  useEffect(() => {
    setCurrentValue(value);
  }, [value]);

  const handleDrag = useCallback(
    (event: MouseEvent) => {
      setCurrentValue(value => {
        // ignore very large jumps in movement, as they often result from
        // mouse acceleration issues
        if (Math.abs(event.movementX) > 100) {
          return value;
        }

        return clamp(min, max, value + event.movementX * step);
      });
    },
    [min, max, step],
  );

  useDocumentEvent(
    'pointerlockchange',
    useCallback(() => {
      if (document.pointerLockElement === inputRef.current) {
        document.addEventListener('pointermove', handleDrag);
      } else {