import * as React from 'react';
import { z as useControlled, m as useValueAsRef, a as useIsoLayoutEffect, b as useStableCallback, A as ios, B as addEventListener, d as createChangeEventDetails, C as none, D as visuallyHiddenInput, E as visuallyHidden, F as inputChange, G as inputClear, H as inputBlur, I as inputPaste, p as activeElement, o as ownerDocument, J as createGenericEventDetails, K as wheel, L as useOnMount, T as Timeout, h as useTimeout, M as incrementPress, N as decrementPress, c as useButton, O as useValueChanged, s as stopEvent, P as keyboard, S as SafeReact } from './useButton-es.js';
import { f as formatErrorMessage, c as useMergedRefs, u as useRenderElement, a as useRefWithInit, N as NOOP, w as warn } from './useRenderElement-es.js';
import { f as fieldValidityMapping, b as useFieldRootContext, a as useFormContext, c as useLabelableId, u as useLabelableContext, d as useRegisterFieldControl } from './useLabelableId-es.js';
import { jsxs, jsx } from 'react/jsx-runtime';
import { o as getWindow } from './floating-ui.utils.dom-es.js';
import { s as stringifyLocale } from './stringifyLocale-es.js';
import { c as clamp } from './clamp-es.js';

/**
 * Returns a function that forces a rerender.
 */
function useForcedRerendering() {
  const [, setState] = React.useState({});
  return React.useCallback(() => {
    setState({});
  }, []);
}

const cache = new Map();
function getFormatter(locale, options) {
  const optionsString = JSON.stringify({
    locale: stringifyLocale(locale),
    options
  });
  const cachedFormatter = cache.get(optionsString);
  if (cachedFormatter) {
    return cachedFormatter;
  }
  const formatter = new Intl.NumberFormat(locale, options);
  cache.set(optionsString, formatter);
  return formatter;
}
function formatNumber(value, locale, options) {
  if (value == null) {
    return '';
  }
  return getFormatter(locale, options).format(value);
}

const NumberFieldRootContext = /*#__PURE__*/React.createContext(undefined);
if (process.env.NODE_ENV !== "production") NumberFieldRootContext.displayName = "NumberFieldRootContext";
function useNumberFieldRootContext() {
  const context = React.useContext(NumberFieldRootContext);
  if (context === undefined) {
    throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: NumberFieldRootContext is missing. NumberField parts must be placed within <NumberField.Root>.' : formatErrorMessage(43));
  }
  return context;
}

const stateAttributesMapping = {
  inputValue: () => null,
  value: () => null,
  ...fieldValidityMapping
};

const HAN_NUMERALS = ['零', '〇', '一', '二', '三', '四', '五', '六', '七', '八', '九'];
// Map Han numeral characters to ASCII digits. Includes both forms of zero.
const HAN_NUMERAL_TO_DIGIT = {
  零: '0',
  〇: '0',
  一: '1',
  二: '2',
  三: '3',
  四: '4',
  五: '5',
  六: '6',
  七: '7',
  八: '8',
  九: '9'
};
const ARABIC_NUMERALS = ['٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩'];
const PERSIAN_NUMERALS = ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹'];
const FULLWIDTH_NUMERALS = ['０', '１', '２', '３', '４', '５', '６', '７', '８', '９'];
const PERCENTAGES = ['%', '٪', '％', '﹪'];
const PERMILLE = ['‰', '؉'];
const UNICODE_MINUS_SIGNS = ['−', '－', '‒', '–', '—', '﹣'];
const UNICODE_PLUS_SIGNS = ['＋', '﹢'];

// Fullwidth punctuation common in CJK inputs
const FULLWIDTH_DECIMAL = '．'; // U+FF0E
const FULLWIDTH_GROUP = '，'; // U+FF0C

const ARABIC_RE = new RegExp(`[${ARABIC_NUMERALS.join('')}]`, 'g');
const PERSIAN_RE = new RegExp(`[${PERSIAN_NUMERALS.join('')}]`, 'g');
const FULLWIDTH_RE = new RegExp(`[${FULLWIDTH_NUMERALS.join('')}]`, 'g');
const HAN_RE = new RegExp(`[${HAN_NUMERALS.join('')}]`, 'g');
const PERCENT_RE = new RegExp(`[${PERCENTAGES.join('')}]`);
const PERMILLE_RE = new RegExp(`[${PERMILLE.join('')}]`);
const PERCENT_GLOBAL_RE = new RegExp(PERCENT_RE.source, 'g');
const PERMILLE_GLOBAL_RE = new RegExp(PERMILLE_RE.source, 'g');

// Detection regexes (non-global to avoid lastIndex side effects), derived from the numeral arrays
// so they can't drift out of sync.
const ARABIC_DETECT_RE = new RegExp(`[${ARABIC_NUMERALS.join('')}]`);
const PERSIAN_DETECT_RE = new RegExp(`[${PERSIAN_NUMERALS.join('')}]`);
const HAN_DETECT_RE = new RegExp(`[${HAN_NUMERALS.join('')}]`);
const FULLWIDTH_DETECT_RE = new RegExp(`[${FULLWIDTH_NUMERALS.join('')}]`);

// Whether the character is a digit in any numeral system the field accepts.
function isNumeralChar(char) {
  return char >= '0' && char <= '9' || ARABIC_DETECT_RE.test(char) || PERSIAN_DETECT_RE.test(char) || HAN_DETECT_RE.test(char) || FULLWIDTH_DETECT_RE.test(char);
}
const BASE_NON_NUMERIC_SYMBOLS = ['.', ',', FULLWIDTH_DECIMAL, FULLWIDTH_GROUP, '٫', '٬'];
const SPACE_SEPARATOR_RE = /\p{Zs}/u;
const PLUS_SIGNS_WITH_ASCII = ['+', ...UNICODE_PLUS_SIGNS];
const MINUS_SIGNS_WITH_ASCII = ['-', ...UNICODE_MINUS_SIGNS];
const escapeRegExp = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const escapeClassChar = s => s.replace(/[-\\\]^]/g, m => `\\${m}`); // escape for use inside [...]

function shiftDecimal(value, exponentDelta) {
  const [coefficient, exponent = '0'] = String(value).split('e');
  return Number(`${coefficient}e${Number(exponent) + exponentDelta}`);
}
const charClassFrom = chars => `[${chars.map(escapeClassChar).join('')}]`;
const ANY_MINUS_CLASS = charClassFrom(['-'].concat(UNICODE_MINUS_SIGNS));
const ANY_PLUS_CLASS = charClassFrom(['+'].concat(UNICODE_PLUS_SIGNS));
const ANY_MINUS_RE = new RegExp(ANY_MINUS_CLASS, 'gu');
const ANY_PLUS_RE = new RegExp(ANY_PLUS_CLASS, 'gu');
const ANY_MINUS_DETECT_RE = new RegExp(ANY_MINUS_CLASS);
const ANY_PLUS_DETECT_RE = new RegExp(ANY_PLUS_CLASS);
function getNumberLocaleDetails(locale, options) {
  const parts = getFormatter(locale, options).formatToParts(11111.1);
  const result = {};
  parts.forEach(part => {
    result[part.type] = part.value;
  });

  // The formatting options may omit the decimal separator (e.g. integer formats), so resolve it
  // from the plain locale formatter. This overrides any options-derived decimal too, which is
  // safe because the separator is locale-determined and identical across format styles.
  getFormatter(locale).formatToParts(0.1).forEach(part => {
    if (part.type === 'decimal') {
      result[part.type] = part.value;
    }
  });
  return result;
}
function parseNumber(formattedNumber, locale, options) {
  if (formattedNumber == null) {
    return null;
  }

  // Normalize control characters and whitespace; remove bidi/format controls
  let input = String(formattedNumber).replace(/\p{Cf}/gu, '').trim();

  // Normalize unicode minus/plus to ASCII, handle leading/trailing signs
  input = input.replace(ANY_MINUS_RE, '-').replace(ANY_PLUS_RE, '+');
  let isNegative = false;

  // Trailing sign, e.g. "1234-" / "1234+"
  const trailing = input.match(/([+-])\s*$/);
  if (trailing) {
    if (trailing[1] === '-') {
      isNegative = true;
    }
    input = input.replace(/([+-])\s*$/, '');
  }
  // Leading sign
  const leading = input.match(/^\s*([+-])/);
  if (leading) {
    if (leading[1] === '-') {
      isNegative = true;
    }
    input = input.replace(/^\s*[+-]/, '');
  }

  // Heuristic locale detection
  let computedLocale = locale;
  if (computedLocale === undefined) {
    if (ARABIC_DETECT_RE.test(input) || PERSIAN_DETECT_RE.test(input)) {
      computedLocale = 'ar';
    } else if (HAN_DETECT_RE.test(input)) {
      computedLocale = 'zh';
    }
  }
  const {
    group,
    decimal,
    currency,
    exponentSeparator
  } = getNumberLocaleDetails(computedLocale, options);

  // Build robust unit regex from all unit parts (such as "km/h")
  const unitParts = getFormatter(computedLocale, options).formatToParts(1).filter(p => p.type === 'unit').map(p => escapeRegExp(p.value));
  const unitRegex = unitParts.length ? new RegExp(unitParts.join('|'), 'g') : null;
  let groupRegex = null;
  if (group) {
    const isSpaceGroup = /\p{Zs}/u.test(group);
    const isApostropheGroup = group === "'" || group === '’';

    // Check if the group separator is a space-like character.
    // If so, we'll replace all such characters with an empty string.
    if (isSpaceGroup) {
      groupRegex = /\p{Zs}/gu;
    } else if (isApostropheGroup) {
      // Some environments format numbers with ASCII apostrophe and others with a curly apostrophe.
      groupRegex = /['’]/g;
    } else {
      groupRegex = new RegExp(escapeRegExp(group), 'g');
    }
  }
  const replacements = [{
    regex: groupRegex,
    replacement: ''
  }, {
    regex: decimal ? new RegExp(escapeRegExp(decimal), 'g') : null,
    replacement: '.'
  },
  // Fullwidth punctuation
  {
    regex: /．/g,
    replacement: '.'
  },
  // FULLWIDTH_DECIMAL
  {
    regex: /，/g,
    replacement: ''
  },
  // FULLWIDTH_GROUP
  // Arabic punctuation
  {
    regex: /٫/g,
    replacement: '.'
  },
  // ARABIC DECIMAL SEPARATOR (U+066B)
  {
    regex: /٬/g,
    replacement: ''
  },
  // ARABIC THOUSANDS SEPARATOR (U+066C)
  // Currency & unit labels
  {
    regex: currency ? new RegExp(escapeRegExp(currency), 'g') : null,
    replacement: ''
  }, {
    regex: unitRegex,
    replacement: ''
  }, {
    regex: PERCENT_GLOBAL_RE,
    replacement: ''
  }, {
    regex: PERMILLE_GLOBAL_RE,
    replacement: ''
  }, {
    regex: exponentSeparator ? new RegExp(escapeRegExp(exponentSeparator), 'g') : null,
    replacement: 'e'
  },
  // Numeral systems to ASCII digits
  {
    regex: ARABIC_RE,
    replacement: ch => String(ARABIC_NUMERALS.indexOf(ch))
  }, {
    regex: PERSIAN_RE,
    replacement: ch => String(PERSIAN_NUMERALS.indexOf(ch))
  }, {
    regex: FULLWIDTH_RE,
    replacement: ch => String(FULLWIDTH_NUMERALS.indexOf(ch))
  }, {
    regex: HAN_RE,
    replacement: ch => HAN_NUMERAL_TO_DIGIT[ch]
  }];
  let unformatted = replacements.reduce((acc, {
    regex,
    replacement
  }) => {
    return regex ? acc.replace(regex, replacement) : acc;
  }, input);

  // Mixed-locale safety: keep only the last '.' as decimal
  const lastDot = unformatted.lastIndexOf('.');
  if (lastDot !== -1) {
    unformatted = `${unformatted.slice(0, lastDot).replace(/\./g, '')}.${unformatted.slice(lastDot + 1).replace(/\./g, '')}`;
  }

  // Guard against Infinity inputs (ASCII and symbol)
  if (/^[-+]?Infinity$/i.test(input) || input.includes('∞')) {
    return null;
  }
  const parseTarget = (isNegative ? '-' : '') + unformatted;
  let num = parseFloat(parseTarget);
  const style = options?.style;
  const isUnitPercent = style === 'unit' && options?.unit === 'percent';
  const hasPercentSymbol = PERCENT_RE.test(formattedNumber) || style === 'percent';
  const hasPermilleSymbol = PERMILLE_RE.test(formattedNumber);
  if (hasPermilleSymbol) {
    num = shiftDecimal(num, -3);
  } else if (!isUnitPercent && hasPercentSymbol) {
    num = shiftDecimal(num, -2);
  }
  if (!Number.isFinite(num)) {
    return null;
  }
  return num;
}

// A relative factor scaled by the step size when snapping (`stepSize * STEP_EPSILON_FACTOR`).
const STEP_EPSILON_FACTOR = 1e-10;
// An absolute cap (in the value's own units) on how far floating-point cleanup may move a value.
// Equal to `STEP_EPSILON_FACTOR` only by coincidence — the two are independent and may diverge.
const MAX_FLOATING_POINT_CLEANUP_DELTA = 1e-10;

// The repo compiles against es2022 Intl types, so model NumberFormat v3 options locally.
// Delete this once tsconfig.base.json includes es2023.

function hasNumberFormatRoundingOptions(format) {
  return format?.maximumFractionDigits != null || format?.minimumFractionDigits != null || format?.maximumSignificantDigits != null || format?.minimumSignificantDigits != null || format?.roundingIncrement != null || format?.roundingMode != null || format?.roundingPriority != null;
}
function removeFloatingPointErrors(value, format) {
  if (!Number.isFinite(value)) {
    return value;
  }
  if (!hasNumberFormatRoundingOptions(format)) {
    // Clean binary floating-point noise (e.g. `0.1 + 0.2`) without discarding legitimate
    // precision. The cleanup is delta-bounded, so it cannot tell noise apart from real digits
    // that fall within the same epsilon: arithmetic that produces sub-epsilon precision (e.g.
    // accumulating a high-significance `step` past the first tick) is normalized to ~15
    // significant digits.
    const roundedValue = parseFloat(value.toPrecision(15));
    const cleanupDelta = Math.abs(roundedValue - value);
    // Cap the cleanup delta so `toPrecision(15)` cannot erase meaningful fractional values
    // at large magnitudes, where the relative epsilon alone is too permissive. The trade-off is
    // that from the `2^19` binade (~5.2e5) up, a single ULP exceeds the absolute cap, so genuine
    // stepping noise is left uncleaned there (e.g. `1000000.1 + 0.2` stays `1000000.2999999999`)
    // rather than risk corrupting real precision.
    const cleanupTolerance = Math.min(Number.EPSILON * Math.max(1, Math.abs(value)), MAX_FLOATING_POINT_CLEANUP_DELTA);
    return cleanupDelta <= cleanupTolerance ? roundedValue : value;
  }
  const formatter = getFormatter('en-US', {
    ...format,
    // These options alter only display decoration, not numeric rounding.
    signDisplay: 'auto',
    currencySign: 'standard',
    notation: format.notation === 'compact' ? 'standard' : format.notation,
    useGrouping: false
  });
  const roundedText = formatter.format(value);
  const roundedValue = parseNumber(roundedText, 'en-US', format);
  if (roundedValue === null) {
    return value;
  }
  return formatter.format(roundedValue) === roundedText ? roundedValue : value;
}
function snapToStep(value, base, step, mode = 'directional') {
  const stepSize = Math.abs(step);
  const direction = Math.sign(step);
  const tolerance = stepSize * STEP_EPSILON_FACTOR * direction;
  const rawSteps = value - base + tolerance;
  if (mode === 'nearest') {
    return base + Math.round(rawSteps / step) * step;
  }
  const snappedSteps = direction > 0 ? Math.floor(rawSteps / stepSize) : Math.ceil(rawSteps / stepSize);
  return base + snappedSteps * stepSize;
}
function toValidatedNumber(value, {
  step,
  minWithDefault,
  maxWithDefault,
  minWithZeroDefault,
  format,
  snapOnStep,
  small,
  clamp: shouldClamp
}) {
  if (value === null) {
    return value;
  }
  let nextValue = value;
  if (step != null && snapOnStep && step !== 0) {
    const base = small || minWithDefault === Number.MIN_SAFE_INTEGER ? minWithZeroDefault : minWithDefault;

    // Snap before clamping so non-step-aligned boundaries stay reachable.
    nextValue = snapToStep(nextValue, base, step, small ? 'nearest' : 'directional');
  }

  // Clamp before rounding so a value just outside a fractional boundary (e.g. `0.4` with
  // `min: 0.6`) is pulled in range first, then clamp again after rounding in case rounding
  // nudges it back out. Both passes are needed for non-integer bounds.
  if (shouldClamp) {
    nextValue = clamp(nextValue, minWithDefault, maxWithDefault);
  }

  // Non-stepping values — parsed input (typing, paste, blur) and externally-supplied controlled
  // values — involve no arithmetic here, so they carry no binary noise to clean and are returned
  // verbatim to keep every digit, including values with more than 15 significant digits. Only
  // stepping/snapping arithmetic can introduce noise (e.g. 0.7 + 0.1), which
  // `removeFloatingPointErrors` cleans.
  if (step == null && !hasNumberFormatRoundingOptions(format)) {
    return nextValue;
  }
  const roundedValue = removeFloatingPointErrors(nextValue, format);
  return shouldClamp ? clamp(roundedValue, minWithDefault, maxWithDefault) : roundedValue;
}

const NumberFieldRoot = /*#__PURE__*/React.forwardRef(function NumberFieldRoot(componentProps, forwardedRef) {
  const {
    id: idProp,
    min,
    max,
    smallStep = 0.1,
    step: stepProp = 1,
    largeStep = 10,
    required = false,
    disabled: disabledProp = false,
    readOnly = false,
    form,
    name: nameProp,
    defaultValue,
    value: valueProp,
    onValueChange: onValueChangeProp,
    onValueCommitted: onValueCommittedProp,
    allowWheelScrub = false,
    snapOnStep = false,
    allowOutOfRange = false,
    format,
    locale,
    render,
    className,
    inputRef: inputRefProp,
    style,
    ...elementProps
  } = componentProps;
  const {
    setDirty,
    validityData,
    disabled: fieldDisabled,
    setFilled,
    invalid,
    name: fieldName,
    state: fieldState,
    validation
  } = useFieldRootContext();
  const {
    clearErrors
  } = useFormContext();
  const disabled = fieldDisabled || disabledProp;
  const name = fieldName ?? nameProp;
  const step = stepProp === 'any' ? 1 : stepProp;
  const [isScrubbing, setIsScrubbing] = React.useState(false);
  const minWithDefault = min ?? Number.MIN_SAFE_INTEGER;
  const maxWithDefault = max ?? Number.MAX_SAFE_INTEGER;
  const minWithZeroDefault = min ?? 0;
  const formatStyle = format?.style;
  const inputRef = React.useRef(null);
  const hiddenInputRef = useMergedRefs(inputRefProp, validation.inputRef);
  const id = useLabelableId({
    id: idProp
  });
  const [valueUnwrapped, setValueUnwrapped] = useControlled({
    controlled: valueProp,
    default: defaultValue,
    name: 'NumberField',
    state: 'value'
  });
  const value = valueUnwrapped ?? null;
  const valueRef = useValueAsRef(value);
  useIsoLayoutEffect(() => {
    setFilled(value !== null);
  }, [setFilled, value]);
  const forceRender = useForcedRerendering();
  const formatOptionsRef = useValueAsRef(format);
  const hasPendingCommitRef = React.useRef(false);
  const onValueCommitted = useStableCallback((nextValue, eventDetails) => {
    hasPendingCommitRef.current = false;
    onValueCommittedProp?.(nextValue, eventDetails);
  });
  const allowInputSyncRef = React.useRef(true);
  const lastChangedValueRef = React.useRef(null);

  // During SSR, the value is formatted on the server, whose locale may differ from the client's
  // locale. This causes a hydration mismatch, which we manually suppress. This is preferable to
  // rendering an empty input field and then updating it with the formatted value, as the user
  // can still see the value prior to hydration, even if it's not formatted correctly.
  const [inputValue, setInputValue] = React.useState(() => formatNumber(value, locale, format));
  const [inputMode, setInputMode] = React.useState('numeric');
  const getAllowedNonNumericKeys = useStableCallback(() => {
    const {
      decimal,
      group,
      currency,
      literal
    } = getNumberLocaleDetails(locale, format);
    const keys = new Set();
    BASE_NON_NUMERIC_SYMBOLS.forEach(symbol => keys.add(symbol));
    if (decimal) {
      keys.add(decimal);
    }
    if (group) {
      keys.add(group);
      if (SPACE_SEPARATOR_RE.test(group)) {
        keys.add(' ');
      }
    }
    const allowPercentSymbols = formatStyle === 'percent' || formatStyle === 'unit' && format?.unit === 'percent';
    const allowPermilleSymbols = formatStyle === 'percent' || formatStyle === 'unit' && format?.unit === 'permille';
    if (allowPercentSymbols) {
      PERCENTAGES.forEach(key => keys.add(key));
    }
    if (allowPermilleSymbols) {
      PERMILLE.forEach(key => keys.add(key));
    }
    if (formatStyle === 'currency' && currency) {
      keys.add(currency);
    }
    if (literal) {
      // Some locales (e.g. de-DE) insert a literal space character between the number
      // and the symbol, so allow those characters to be typed/removed.
      Array.from(literal).forEach(char => keys.add(char));
      if (SPACE_SEPARATOR_RE.test(literal)) {
        keys.add(' ');
      }
    }

    // Allow plus sign in all cases; minus sign when negatives are valid, or when out-of-range
    // entry is allowed so native underflow validation can be triggered from the keyboard.
    PLUS_SIGNS_WITH_ASCII.forEach(key => keys.add(key));
    if (minWithDefault < 0 || allowOutOfRange) {
      MINUS_SIGNS_WITH_ASCII.forEach(key => keys.add(key));
    }
    return keys;
  });
  const getStepAmount = useStableCallback(event => {
    if (event?.altKey) {
      return smallStep;
    }
    if (event?.shiftKey) {
      return largeStep;
    }
    return step;
  });
  const setValue = useStableCallback((unvalidatedValue, details) => {
    const eventWithOptionalKeyState = details.event;
    const dir = details.direction;

    // Direct text entry (typing, pasting, clearing, autofill) behaves natively; step-based
    // interactions (keyboard arrows, buttons, wheel, scrub) do not.
    const isInputReason = details.reason === inputChange || details.reason === inputClear || details.reason === inputBlur || details.reason === inputPaste || details.reason === none;

    // Only allow out-of-range values for direct text entry. Step-based interactions still clamp.
    const shouldClampValue = !allowOutOfRange || !isInputReason;
    const validatedValue = toValidatedNumber(unvalidatedValue, {
      step: dir ? getStepAmount(eventWithOptionalKeyState) * dir : undefined,
      format: formatOptionsRef.current,
      minWithDefault,
      maxWithDefault,
      minWithZeroDefault,
      snapOnStep,
      small: eventWithOptionalKeyState?.altKey ?? false,
      clamp: shouldClampValue
    });

    // Notify about a change even when the numeric value is unchanged for input reasons: the
    // typed text may clamp/snap to the current value, or differ while validation normalizes
    // it back to the existing value.
    const shouldFireChange = validatedValue !== value || isInputReason && (unvalidatedValue !== value || allowInputSyncRef.current === false);
    if (shouldFireChange) {
      onValueChangeProp?.(validatedValue, details);
      if (details.isCanceled) {
        // Report a vetoed change as not applied, so callers don't commit a value never stored.
        return false;
      }
      setValueUnwrapped(validatedValue);
      setDirty(validatedValue !== validityData.initialValue);
      hasPendingCommitRef.current = true;
    }
    lastChangedValueRef.current = validatedValue;

    // Keep the visible input in sync immediately when programmatic changes occur
    // (increment/decrement, wheel, etc). During direct typing we don't want
    // to overwrite the user-provided text until blur, so we gate on
    // `allowInputSyncRef`.
    if (allowInputSyncRef.current) {
      setInputValue(formatNumber(validatedValue, locale, format));
    }

    // Formatting can change even if the numeric value hasn't, so ensure a re-render when needed.
    forceRender();
    return shouldFireChange;
  });
  const incrementValue = useStableCallback((amount, {
    direction,
    currentValue,
    event,
    reason
  }) => {
    const prevValue = currentValue == null ? valueRef.current : currentValue;
    const nativeEvent = event;
    if (typeof prevValue !== 'number') {
      // Seed an empty field with 0; `setValue` clamps it to the in-range value nearest 0
      // (e.g. `max` for a negative range). No `direction`: the seed isn't a step, so it must
      // not be directionally snapped.
      return setValue(0, createChangeEventDetails(reason, nativeEvent));
    }
    return setValue(prevValue + amount * direction, createChangeEventDetails(reason, nativeEvent, undefined, {
      direction
    }));
  });

  // We need to update the input value when the external `value` prop changes. This ends up acting
  // as a single source of truth to update the input value, bypassing the need to manually set it in
  // each event handler.
  // This is done inside a layout effect as an alternative to the technique to set state during
  // render as we're accessing a ref, which must be inside an effect.
  // https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes
  //
  // ESLint is disabled because it needs to run even if the parsed value hasn't changed, since the
  // value still can be formatted differently.
  // eslint-disable-next-line react-hooks/exhaustive-deps
  useIsoLayoutEffect(function syncFormattedInputValueOnValueChange() {
    // This ensures the value is only updated on blur rather than every keystroke, but still
    // allows the input value to be updated when the value is changed externally.
    if (!allowInputSyncRef.current) {
      return;
    }
    const nextInputValue = formatNumber(value, locale, format);
    if (nextInputValue !== inputValue) {
      setInputValue(nextInputValue);
    }
  });
  useIsoLayoutEffect(function setDynamicInputModeForIOS() {
    if (!ios) {
      return;
    }

    // iOS numeric software keyboard doesn't have a minus key, so we need to use the default
    // keyboard to let the user input a negative number.
    let computedInputMode = 'text';
    if (minWithDefault >= 0) {
      // iOS numeric software keyboard doesn't have a decimal key for "numeric" input mode, but
      // this is better than the "text" input if possible to use.
      computedInputMode = 'decimal';
    }
    setInputMode(computedInputMode);
  }, [minWithDefault]);

  // React attaches `onWheel` as a passive listener, so calling `preventDefault` there is ignored.
  // Attach a native (non-passive) `wheel` listener to the input instead to prevent page scrolling.
  React.useEffect(function registerElementWheelListener() {
    const element = inputRef.current;
    if (disabled || readOnly || !allowWheelScrub || !element) {
      return undefined;
    }
    function handleWheel(event) {
      if (
      // Allow pinch-zooming.
      event.ctrlKey || activeElement(ownerDocument(inputRef.current)) !== inputRef.current) {
        return;
      }

      // Prevent the default behavior to avoid scrolling the page.
      event.preventDefault();
      allowInputSyncRef.current = true;
      const amount = getStepAmount(event);

      // Each wheel turn is a discrete, final change, so commit it immediately like keyboard
      // steps (gated on an actual change so boundary no-ops don't commit).
      const changed = incrementValue(amount, {
        direction: event.deltaY > 0 ? -1 : 1,
        event,
        reason: 'wheel'
      });
      if (changed) {
        onValueCommitted(lastChangedValueRef.current ?? valueRef.current, createGenericEventDetails(wheel, event));
      }
    }
    return addEventListener(element, 'wheel', handleWheel);
  }, [allowWheelScrub, incrementValue, disabled, readOnly, getStepAmount, onValueCommitted, lastChangedValueRef, valueRef]);
  const state = React.useMemo(() => ({
    ...fieldState,
    disabled,
    readOnly,
    required,
    value,
    inputValue,
    scrubbing: isScrubbing
  }), [fieldState, disabled, readOnly, required, value, inputValue, isScrubbing]);
  const contextValue = React.useMemo(() => ({
    inputRef,
    inputValue,
    value,
    minWithDefault,
    maxWithDefault,
    disabled,
    readOnly,
    id,
    setValue,
    incrementValue,
    getStepAmount,
    allowInputSyncRef,
    formatOptionsRef,
    valueRef,
    lastChangedValueRef,
    hasPendingCommitRef,
    name,
    nameProp,
    required,
    invalid,
    inputMode,
    getAllowedNonNumericKeys,
    min,
    max,
    setInputValue,
    locale,
    isScrubbing,
    setIsScrubbing,
    state,
    onValueCommitted
  }), [inputRef, inputValue, value, minWithDefault, maxWithDefault, disabled, readOnly, id, setValue, incrementValue, getStepAmount, formatOptionsRef, valueRef, name, nameProp, required, invalid, inputMode, getAllowedNonNumericKeys, min, max, setInputValue, locale, isScrubbing, state, onValueCommitted]);
  const element = useRenderElement('div', componentProps, {
    ref: forwardedRef,
    state,
    props: elementProps,
    stateAttributesMapping
  });
  return /*#__PURE__*/jsxs(NumberFieldRootContext.Provider, {
    value: contextValue,
    children: [element, /*#__PURE__*/jsx("input", {
      ...validation.getValidationProps(disabled, {
        onFocus() {
          inputRef.current?.focus();
        },
        onChange(event) {
          // Workaround for https://github.com/facebook/react/issues/9023
          if (event.nativeEvent.defaultPrevented || disabled || readOnly) {
            return;
          }

          // Handle browser autofill.
          const nextValue = event.currentTarget.valueAsNumber;
          const parsedValue = Number.isNaN(nextValue) ? null : nextValue;
          const details = createChangeEventDetails(none, event.nativeEvent);

          // `setValue` updates the dirty flag from the stored (clamped) value, so validate with
          // that same value rather than the raw autofilled one.
          setValue(parsedValue, details);
          clearErrors(name);
          validation.change(lastChangedValueRef.current ?? parsedValue);
        }
      }),
      ref: hiddenInputRef,
      type: "number",
      form: form,
      name: name,
      value: value ?? '',
      min: min,
      max: max
      // stepMismatch validation is broken unless an explicit `min` is added.
      // See https://github.com/facebook/react/issues/12334.
      ,
      step: stepProp,
      disabled: disabled,
      readOnly: readOnly,
      required: required,
      "aria-hidden": true,
      tabIndex: -1,
      style: name ? visuallyHiddenInput : visuallyHidden,
      suppressHydrationWarning: true
    })]
  });
});
if (process.env.NODE_ENV !== "production") NumberFieldRoot.displayName = "NumberFieldRoot"; // `none` is kept for consistency with other components even though the number field never
// commits with it.

/**
 * Groups the input with the increment and decrement buttons.
 * Renders a `<div>` element.
 *
 * Documentation: [Base UI Number Field](https://base-ui.com/react/components/number-field)
 */
const NumberFieldGroup = /*#__PURE__*/React.forwardRef(function NumberFieldGroup(componentProps, forwardedRef) {
  const {
    render,
    className,
    style,
    ...elementProps
  } = componentProps;
  const {
    state
  } = useNumberFieldRootContext();
  const element = useRenderElement('div', componentProps, {
    ref: forwardedRef,
    state,
    props: [{
      role: 'group'
    }, elementProps],
    stateAttributesMapping
  });
  return element;
});
if (process.env.NODE_ENV !== "production") NumberFieldGroup.displayName = "NumberFieldGroup";

const EMPTY = 0;
class Interval extends Timeout {
  static create() {
    return new Interval();
  }

  /**
   * Executes `fn` at `delay` interval, clearing any previously scheduled call.
   */
  start(delay, fn) {
    this.clear();
    this.currentId = setInterval(() => {
      fn();
    }, delay);
  }
  clear = () => {
    if (this.currentId !== EMPTY) {
      clearInterval(this.currentId);
      this.currentId = EMPTY;
    }
  };
}

/**
 * A `setInterval` with automatic cleanup and guard.
 */
function useInterval() {
  const timeout = useRefWithInit(Interval.create).current;
  useOnMount(timeout.disposeEffect);
  return timeout;
}

const DEFAULT_TICK_DELAY = 60;
const DEFAULT_START_DELAY = 400;
const DEFAULT_SCROLL_DISTANCE = 8;
const TOUCH_TIMEOUT = 50;
const MAX_POINTER_MOVES_AFTER_TOUCH = 3;

// Treat pen as touch-like to avoid forcing the software keyboard on stylus taps.
// Linux Chrome may emit "pen" historically for mouse usage due to a bug, but the touch path
// still works with minor behavioral differences.
function isTouchLikePointerType(pointerType) {
  return pointerType === 'touch' || pointerType === 'pen';
}
/**
 * Adds press-and-hold behavior to a button element.
 * On pointer down, performs one action immediately, then after a delay starts
 * continuous repeated actions at a fixed interval. Handles mouse, touch, and pen
 * inputs correctly, including Android-specific quirks.
 */
function usePressAndHold(params) {
  const {
    disabled,
    readOnly = false,
    tick,
    onStop,
    tickDelay = DEFAULT_TICK_DELAY,
    startDelay = DEFAULT_START_DELAY,
    scrollDistance = DEFAULT_SCROLL_DISTANCE,
    elementRef
  } = params;
  const startTickTimeout = useTimeout();
  const tickInterval = useInterval();
  const intentionalTouchCheckTimeout = useTimeout();
  const isPressedRef = React.useRef(false);
  const movesAfterTouchRef = React.useRef(0);
  const downCoordsRef = React.useRef({
    x: 0,
    y: 0
  });
  const isTouchingButtonRef = React.useRef(false);
  const ignoreClickRef = React.useRef(false);
  const pointerTypeRef = React.useRef('');
  const unsubscribeFromGlobalContextMenuRef = React.useRef(NOOP);
  const unsubscribeFromGlobalPointerUpRef = React.useRef(NOOP);
  const stopAutoChange = useStableCallback(() => {
    intentionalTouchCheckTimeout.clear();
    startTickTimeout.clear();
    tickInterval.clear();
    unsubscribeFromGlobalContextMenuRef.current();
    movesAfterTouchRef.current = 0;
  });
  function startAutoChange(triggerNativeEvent) {
    stopAutoChange();
    const element = elementRef.current;
    if (!element) {
      return;
    }
    const win = getWindow(element);
    function handleContextMenu(event) {
      event.preventDefault();
    }

    // A global context menu listener is necessary to prevent the context menu from
    // appearing when the touch is slightly outside of the element's hit area.
    unsubscribeFromGlobalContextMenuRef.current = addEventListener(win, 'contextmenu', handleContextMenu);

    // The release listener stays registered through `stopAutoChange` so a hold that auto-stops at
    // a boundary (a repeat tick returning `false`) still fires `onStop` on release. Replace any
    // existing one first so a mouseleave/mouseenter cycle during a hold doesn't stack listeners
    // (which would otherwise fire `onStop` more than once on release).
    unsubscribeFromGlobalPointerUpRef.current();
    unsubscribeFromGlobalPointerUpRef.current = addEventListener(win, 'pointerup', event => {
      isPressedRef.current = false;
      stopAutoChange();
      onStop?.(event);
    }, {
      once: true
    });
    if (!tick(triggerNativeEvent)) {
      stopAutoChange();
      return;
    }
    startTickTimeout.start(startDelay, () => {
      tickInterval.start(tickDelay, () => {
        if (!tick(triggerNativeEvent)) {
          stopAutoChange();
        }
      });
    });
  }
  React.useEffect(() => () => {
    stopAutoChange();
    unsubscribeFromGlobalPointerUpRef.current();
  }, [stopAutoChange]);
  const pointerHandlers = {
    onTouchStart() {
      isTouchingButtonRef.current = true;
    },
    onTouchEnd() {
      isTouchingButtonRef.current = false;
    },
    onPointerDown(event) {
      const isMainButton = !event.button || event.button === 0;
      if (event.defaultPrevented || !isMainButton || disabled || readOnly) {
        return;
      }
      pointerTypeRef.current = event.pointerType;
      ignoreClickRef.current = false;
      isPressedRef.current = true;
      downCoordsRef.current = {
        x: event.clientX,
        y: event.clientY
      };
      const isTouchPointer = isTouchLikePointerType(event.pointerType);
      if (!isTouchPointer) {
        event.preventDefault();
        startAutoChange(event.nativeEvent);
      } else {
        // Check if the pointerdown was intentional and not the result of a scroll or
        // pinch-zoom. In that case, we don't want to start the auto-change sequence.
        intentionalTouchCheckTimeout.start(TOUCH_TIMEOUT, () => {
          const moves = movesAfterTouchRef.current;
          movesAfterTouchRef.current = 0;
          // Only start auto-change if the touch is still pressed (prevents races
          // with pointerup occurring before the timeout fires on quick taps).
          const stillPressed = isPressedRef.current;
          if (stillPressed && moves < MAX_POINTER_MOVES_AFTER_TOUCH) {
            startAutoChange(event.nativeEvent);
            ignoreClickRef.current = true; // synthesized click after hold should be ignored
          } else {
            // No auto-change (simple tap or scroll gesture), allow the click handler
            // to perform a single action.
            ignoreClickRef.current = false;
            stopAutoChange();
          }
        });
      }
    },
    onPointerUp(event) {
      // Ensure we mark the press as released for touch flows even if auto-change never
      // started, so the delayed auto-change check won't start after a quick tap.
      if (isTouchLikePointerType(event.pointerType)) {
        isPressedRef.current = false;
      }
    },
    onPointerMove(event) {
      if (disabled || readOnly || !isTouchLikePointerType(event.pointerType) || !isPressedRef.current) {
        return;
      }
      if (movesAfterTouchRef.current != null) {
        movesAfterTouchRef.current += 1;
      }
      const {
        x,
        y
      } = downCoordsRef.current;
      const dx = x - event.clientX;
      const dy = y - event.clientY;
      if (dx ** 2 + dy ** 2 > scrollDistance ** 2) {
        stopAutoChange();
      }
    },
    onMouseEnter(event) {
      if (event.defaultPrevented || disabled || readOnly || !isPressedRef.current || isTouchingButtonRef.current || isTouchLikePointerType(pointerTypeRef.current)) {
        return;
      }
      startAutoChange(event.nativeEvent);
    },
    onMouseLeave() {
      if (isTouchingButtonRef.current) {
        return;
      }
      stopAutoChange();
    },
    onMouseUp() {
      if (isTouchingButtonRef.current) {
        return;
      }
      stopAutoChange();
    }
  };
  const shouldSkipClick = useStableCallback(event => {
    if (event.defaultPrevented) {
      return true;
    }
    if (isTouchLikePointerType(pointerTypeRef.current)) {
      return ignoreClickRef.current;
    }
    return event.detail !== 0;
  });
  return {
    pointerHandlers,
    shouldSkipClick
  };
}

const CHANGE_VALUE_TICK_DELAY = 60;
const START_AUTO_CHANGE_DELAY = 400;
const SCROLLING_POINTER_MOVE_DISTANCE = 8;

const SELECT_NONE_STYLE = {
  WebkitUserSelect: 'none',
  userSelect: 'none'
};
function useNumberFieldButton(params) {
  const {
    allowInputSyncRef,
    disabled,
    formatOptionsRef,
    getStepAmount,
    id,
    incrementValue,
    inputRef,
    inputValue,
    isIncrement,
    locale,
    readOnly,
    setValue,
    valueRef,
    lastChangedValueRef,
    onValueCommitted
  } = params;
  const pressReason = isIncrement ? incrementPress : decrementPress;
  function commitValue(nativeEvent) {
    const shouldCommitInputValue = !allowInputSyncRef.current;
    allowInputSyncRef.current = true;
    if (!shouldCommitInputValue) {
      // The input is already synced, so step from the authoritative numeric value rather than
      // re-parsing the rounded display text. Refresh the commit ref to the current value so a
      // subsequent canceled step can't commit a stale `lastChangedValueRef` left over from an
      // earlier change (the `setValue` that used to refresh it is now skipped on this path).
      lastChangedValueRef.current = valueRef.current;
      return;
    }

    // The input is dirty but not yet blurred, so the value won't have been committed.
    const parsedValue = parseNumber(inputValue, locale, formatOptionsRef.current);
    if (parsedValue !== null) {
      // Sync the dirty typed value with no direction so it isn't directionally snapped
      // (`snapOnStep`) before the real increment/decrement runs, which would otherwise emit a
      // spurious intermediate value.
      const details = createChangeEventDetails(pressReason, nativeEvent);
      setValue(parsedValue, details);

      // Only sync the ref base when the commit wasn't canceled, so a subsequent increment in the
      // same interaction steps from the value actually applied.
      if (!details.isCanceled) {
        valueRef.current = parsedValue;
      }
    }
  }
  const {
    pointerHandlers,
    shouldSkipClick
  } = usePressAndHold({
    disabled: disabled || readOnly,
    elementRef: inputRef,
    tickDelay: CHANGE_VALUE_TICK_DELAY,
    startDelay: START_AUTO_CHANGE_DELAY,
    scrollDistance: SCROLLING_POINTER_MOVE_DISTANCE,
    tick(triggerEvent) {
      const amount = getStepAmount(triggerEvent);
      return incrementValue(amount, {
        direction: isIncrement ? 1 : -1,
        event: triggerEvent,
        reason: pressReason
      });
    },
    onStop(nativeEvent) {
      // `onStop` fires on every release; fall back to the current value when no tick changed it.
      // Step interactions never commit `null`, so the `??` can't mask a legitimate null commit.
      const committed = lastChangedValueRef.current ?? valueRef.current;
      onValueCommitted(committed, createGenericEventDetails(pressReason, nativeEvent));
    }
  });
  const props = {
    disabled,
    'aria-label': isIncrement ? 'Increase' : 'Decrease',
    'aria-controls': id,
    // Keyboard users shouldn't have access to the buttons, since they can use the input element
    // to change the value. On the other hand, `aria-hidden` is not applied because touch screen
    // readers should be able to use the buttons.
    tabIndex: -1,
    style: SELECT_NONE_STYLE,
    ...pointerHandlers,
    onClick(event) {
      const isDisabled = disabled || readOnly;
      if (event.defaultPrevented || isDisabled || shouldSkipClick(event)) {
        return;
      }
      commitValue(event.nativeEvent);
      const amount = getStepAmount(event);
      const prev = valueRef.current;
      incrementValue(amount, {
        direction: isIncrement ? 1 : -1,
        event: event.nativeEvent,
        reason: pressReason
      });
      const committed = lastChangedValueRef.current ?? valueRef.current;
      if (committed !== prev) {
        onValueCommitted(committed, createGenericEventDetails(pressReason, event.nativeEvent));
      }
    },
    onPointerDown(event) {
      const isMainButton = !event.button || event.button === 0;
      if (event.defaultPrevented || readOnly || !isMainButton || disabled) {
        return;
      }

      // Sync dirty input value before starting the hold sequence.
      commitValue(event.nativeEvent);
      // Treat `lastChangedValueRef` as a per-hold result slot. If the first tick is a no-op or is
      // canceled, `onStop` should fall back to the current value, not a previous interaction.
      lastChangedValueRef.current = null;
      if (!isTouchLikePointerType(event.pointerType)) {
        // Focus the input so the user can continue with keyboard interactions.
        inputRef.current?.focus();
      }
      pointerHandlers.onPointerDown(event);
    }
  };
  return props;
}

/**
 * Shared implementation for the increment and decrement stepper buttons. They differ only in the
 * direction they step and the boundary (`max` vs `min`) at which they become disabled.
 */
function useNumberFieldStepperButton(componentProps, forwardedRef, isIncrement) {
  const {
    render,
    className,
    disabled: disabledProp = false,
    nativeButton = true,
    style,
    ...elementProps
  } = componentProps;
  const {
    allowInputSyncRef,
    disabled: contextDisabled,
    formatOptionsRef,
    getStepAmount,
    id,
    incrementValue,
    inputRef,
    inputValue,
    maxWithDefault,
    minWithDefault,
    readOnly,
    setValue,
    state,
    value,
    valueRef,
    locale,
    lastChangedValueRef,
    onValueCommitted
  } = useNumberFieldRootContext();
  const isAtBoundary = value != null && (isIncrement ? value >= maxWithDefault : value <= minWithDefault);
  const disabled = disabledProp || contextDisabled || isAtBoundary;
  const props = useNumberFieldButton({
    isIncrement,
    inputRef,
    inputValue,
    disabled,
    readOnly,
    id,
    setValue,
    getStepAmount,
    incrementValue,
    allowInputSyncRef,
    formatOptionsRef,
    valueRef,
    locale,
    lastChangedValueRef,
    onValueCommitted
  });
  const {
    getButtonProps,
    buttonRef
  } = useButton({
    // Read-only steppers are exposed as unavailable through button disabled semantics, while
    // `data-readonly` (from `state`) is preserved for styling. `aria-readonly` isn't valid on the
    // `button` role, so it's intentionally not set.
    disabled: disabled || readOnly,
    native: nativeButton,
    focusableWhenDisabled: true
  });
  const buttonState = {
    ...state,
    disabled
  };
  return useRenderElement('button', componentProps, {
    ref: [forwardedRef, buttonRef],
    state: buttonState,
    props: [props, elementProps, getButtonProps],
    stateAttributesMapping
  });
}

/**
 * A stepper button that increases the field value when clicked.
 * Renders a `<button>` element.
 *
 * Documentation: [Base UI Number Field](https://base-ui.com/react/components/number-field)
 */
const NumberFieldIncrement = /*#__PURE__*/React.forwardRef(function NumberFieldIncrement(componentProps, forwardedRef) {
  return useNumberFieldStepperButton(componentProps, forwardedRef, true);
});
if (process.env.NODE_ENV !== "production") NumberFieldIncrement.displayName = "NumberFieldIncrement";

/**
 * A stepper button that decreases the field value when clicked.
 * Renders a `<button>` element.
 *
 * Documentation: [Base UI Number Field](https://base-ui.com/react/components/number-field)
 */
const NumberFieldDecrement = /*#__PURE__*/React.forwardRef(function NumberFieldDecrement(componentProps, forwardedRef) {
  return useNumberFieldStepperButton(componentProps, forwardedRef, false);
});
if (process.env.NODE_ENV !== "production") NumberFieldDecrement.displayName = "NumberFieldDecrement";

const NAVIGATE_KEYS = new Set(['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'Tab', 'Enter', 'Escape']);

/**
 * The native input control in the number field.
 * Renders an `<input>` element.
 *
 * Documentation: [Base UI Number Field](https://base-ui.com/react/components/number-field)
 */
const NumberFieldInput = /*#__PURE__*/React.forwardRef(function NumberFieldInput(componentProps, forwardedRef) {
  const {
    render,
    className,
    style,
    ...elementProps
  } = componentProps;
  const {
    allowInputSyncRef,
    disabled,
    formatOptionsRef,
    getAllowedNonNumericKeys,
    getStepAmount,
    id,
    incrementValue,
    inputMode,
    inputValue,
    max,
    min,
    name,
    nameProp,
    readOnly,
    required,
    setValue,
    state,
    setInputValue,
    locale,
    inputRef,
    value,
    onValueCommitted,
    lastChangedValueRef,
    hasPendingCommitRef,
    valueRef
  } = useNumberFieldRootContext();
  const {
    clearErrors
  } = useFormContext();
  const {
    validationMode,
    setTouched,
    setFocused,
    invalid,
    shouldValidateOnChange,
    validation
  } = useFieldRootContext();
  const {
    labelId
  } = useLabelableContext();
  const hasTouchedInputRef = React.useRef(false);
  const blockRevalidationRef = React.useRef(false);
  const pendingCaretRef = React.useRef(null);
  useRegisterFieldControl(inputRef, id, value, undefined, !disabled, nameProp);

  // After a paste splices text into the controlled value, the browser would otherwise drop the
  // caret at the end of the new value. Restore it just after the inserted text.
  useIsoLayoutEffect(() => {
    if (pendingCaretRef.current != null) {
      const caret = pendingCaretRef.current;
      pendingCaretRef.current = null;
      inputRef.current?.setSelectionRange(caret, caret);
    }
  });
  useValueChanged(value, () => {
    clearErrors(name);
    if (blockRevalidationRef.current && !shouldValidateOnChange()) {
      blockRevalidationRef.current = false;
      return;
    }
    validation.change(value);
  });
  const inputProps = {
    id,
    required,
    disabled,
    readOnly,
    inputMode,
    value: inputValue,
    type: 'text',
    autoComplete: 'off',
    autoCorrect: 'off',
    spellCheck: 'false',
    'aria-roledescription': 'Number field',
    'aria-invalid': !disabled && invalid ? true : undefined,
    'aria-labelledby': labelId,
    // If the server's locale does not match the client's locale, the formatting may not match,
    // causing a hydration mismatch.
    suppressHydrationWarning: true,
    onFocus(event) {
      // Read-only inputs are still focusable; only the value-changing handlers stay gated on it.
      if (event.defaultPrevented || disabled) {
        return;
      }
      setFocused(true);
      if (hasTouchedInputRef.current) {
        return;
      }
      hasTouchedInputRef.current = true;

      // Browsers set selection at the start of the input field by default. We want to set it at
      // the end for the first focus.
      const target = event.currentTarget;
      const length = target.value.length;
      target.setSelectionRange(length, length);
    },
    onBlur(event) {
      if (event.defaultPrevented || disabled) {
        return;
      }
      setTouched(true);
      setFocused(false);
      if (readOnly) {
        return;
      }
      const hadManualInput = !allowInputSyncRef.current;
      const hadPendingProgrammaticChange = hasPendingCommitRef.current;
      allowInputSyncRef.current = true;
      if (inputValue.trim() === '') {
        const clearDetails = createChangeEventDetails(inputClear, event.nativeEvent);
        setValue(null, clearDetails);
        // Respect a canceled clear, mirroring the non-empty blur path below.
        if (clearDetails.isCanceled) {
          return;
        }
        if (validationMode === 'onBlur') {
          validation.commit(null);
        }
        // Don't report a commit when blurring an already-empty field that the user never
        // interacted with: nothing was cleared and no programmatic change is pending.
        if (hadManualInput || hadPendingProgrammaticChange || value !== null) {
          onValueCommitted(null, createGenericEventDetails(inputClear, event.nativeEvent));
        }
        return;
      }
      const formatOptions = formatOptionsRef.current;
      const parsedValue = parseNumber(inputValue, locale, formatOptions);
      if (parsedValue === null) {
        return;
      }

      // Avoid applying Intl's default precision unless the format opts into rounding.
      const hasRoundingOptions = hasNumberFormatRoundingOptions(formatOptions);
      let committed;
      if (!hadManualInput && !hasRoundingOptions) {
        // No rounding options and no manual edit: the visible text is purely formatted
        // display, so keep the authoritative numeric value as-is rather than re-parsing the
        // rounded text and discarding precision (e.g. focus/blur with no edits, or blur after
        // a programmatic change).
        committed = value;
      } else if (hasRoundingOptions) {
        // Explicit rounding options apply to the committed value, whether typed or external.
        committed = removeFloatingPointErrors(parsedValue, formatOptions);
      } else {
        committed = parsedValue;
      }
      const nextEventDetails = createGenericEventDetails(inputBlur, event.nativeEvent);
      const shouldUpdateValue = value !== committed;
      const shouldCommit = hadManualInput || shouldUpdateValue || hadPendingProgrammaticChange;

      // Use the stored value after `setValue` clamps it.
      let committedValue = committed;
      if (shouldUpdateValue) {
        const changeDetails = createChangeEventDetails(inputBlur, event.nativeEvent);
        blockRevalidationRef.current = true;
        setValue(committed, changeDetails);
        if (changeDetails.isCanceled) {
          blockRevalidationRef.current = false;
          return;
        }
        committedValue = lastChangedValueRef.current ?? committed;
        // If validation normalized back to the current value, `useValueChanged` won't fire to
        // reset the flag, so reset it here or the next external change won't revalidate.
        if (committedValue === value) {
          blockRevalidationRef.current = false;
        }
      }
      if (validationMode === 'onBlur') {
        validation.commit(committedValue);
      }
      if (shouldCommit) {
        onValueCommitted(committedValue, nextEventDetails);
      }

      // Normalize only the displayed text
      const canonicalText = formatNumber(committedValue, locale, formatOptions);
      if (inputValue !== canonicalText) {
        setInputValue(canonicalText);
      }
    },
    onChange(event) {
      // Workaround for https://github.com/facebook/react/issues/9023
      if (event.nativeEvent.defaultPrevented) {
        return;
      }
      allowInputSyncRef.current = false;
      const targetValue = event.currentTarget.value;
      if (targetValue.trim() === '') {
        setInputValue(targetValue);
        setValue(null, createChangeEventDetails(inputClear, event.nativeEvent));
        return;
      }

      // Update the input text immediately and only fire onValueChange if the typed value is
      // currently parseable into a number. This preserves good UX for IME
      // composition/partial input while still providing live numeric updates when possible.
      const allowedNonNumericKeys = getAllowedNonNumericKeys();
      const isValidCharacterString = Array.from(targetValue).every(ch => isNumeralChar(ch) || ANY_MINUS_DETECT_RE.test(ch) || allowedNonNumericKeys.has(ch));
      if (!isValidCharacterString) {
        return;
      }
      const parsedValue = parseNumber(targetValue, locale, formatOptionsRef.current);
      setInputValue(targetValue);
      if (parsedValue !== null) {
        setValue(parsedValue, createChangeEventDetails(inputChange, event.nativeEvent));
      }
    },
    onKeyDown(event) {
      if (event.defaultPrevented || readOnly || disabled) {
        return;
      }
      const nativeEvent = event.nativeEvent;

      // Snapshot the dirty state without clearing it: navigation/allowed keys (ArrowLeft, Tab,
      // Enter, Escape, …) return early without changing the value, so marking the input synced
      // here would wrongly discard dirty-input authority. Only the value-changing branches below
      // mark it synced.
      const hadManualInput = !allowInputSyncRef.current;
      const allowedNonNumericKeys = getAllowedNonNumericKeys();
      let isAllowedNonNumericKey = allowedNonNumericKeys.has(event.key);
      const {
        decimal,
        currency,
        percentSign
      } = getNumberLocaleDetails(locale, formatOptionsRef.current);
      const selectionStart = event.currentTarget.selectionStart;
      const selectionEnd = event.currentTarget.selectionEnd;
      const isAllSelected = selectionStart === 0 && selectionEnd === inputValue.length;
      const selectionContainsIndex = index => selectionStart != null && selectionEnd != null && index >= selectionStart && index < selectionEnd;

      // Only allow a single sign character: permit it when there is no existing sign of either
      // kind, when all text is selected, or when the selection covers the existing sign so it's
      // being replaced.
      const signGroups = [[ANY_MINUS_DETECT_RE, ANY_MINUS_RE], [ANY_PLUS_DETECT_RE, ANY_PLUS_RE]];
      signGroups.forEach(([detectRe, globalRe]) => {
        if (detectRe.test(event.key) && Array.from(allowedNonNumericKeys).some(k => detectRe.test(k))) {
          const existingIndex = inputValue.search(globalRe);
          const isReplacingExisting = existingIndex !== -1 && selectionContainsIndex(existingIndex);
          isAllowedNonNumericKey = !(ANY_MINUS_DETECT_RE.test(inputValue) || ANY_PLUS_DETECT_RE.test(inputValue)) || isAllSelected || isReplacingExisting;
        }
      });

      // Only allow one of each symbol.
      [decimal, currency, percentSign].forEach(symbol => {
        if (event.key === symbol) {
          const symbolIndex = inputValue.indexOf(symbol);
          const isSymbolHighlighted = selectionContainsIndex(symbolIndex);
          isAllowedNonNumericKey = !inputValue.includes(symbol) || isAllSelected || isSymbolHighlighted;
        }
      });
      const isNavigateKey = NAVIGATE_KEYS.has(event.key);
      // Alt+ArrowUp/ArrowDown selects smallStep, so don't treat it as a bypass modifier.
      const isStepKey = event.key === 'ArrowUp' || event.key === 'ArrowDown';
      if (
      // Allow composition events (e.g., pinyin)
      // event.nativeEvent.isComposing does not work in Safari:
      // https://bugs.webkit.org/show_bug.cgi?id=165004
      event.which === 229 || event.altKey && !isStepKey || event.ctrlKey || event.metaKey || isAllowedNonNumericKey || isNumeralChar(event.key) || isNavigateKey) {
        return;
      }
      const willSetHome = event.key === 'Home' && min != null;
      const willSetEnd = event.key === 'End' && max != null;

      // Let the browser handle multi-character keys we don't act on (PageUp, Insert, F-keys,
      // Home/End without min/max); invalid single characters are still blocked below.
      if (event.key.length > 1 && !isStepKey && !willSetHome && !willSetEnd) {
        return;
      }

      // Step from the authoritative numeric value unless the input has unsaved manual edits.
      // When the text is already synced, parsing the rounded display would collapse precision,
      // so pass no `currentValue` and let `incrementValue` fall back to the numeric state
      // (mirrors the button path).
      const currentValue = hadManualInput ? parseNumber(inputValue, locale, formatOptionsRef.current) : null;
      const amount = getStepAmount(event);

      // Prevent insertion of text or caret from moving.
      stopEvent(event);
      const commitDetails = createGenericEventDetails(keyboard, nativeEvent);
      let changed = false;
      if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
        allowInputSyncRef.current = true;

        // When stepping from the synced numeric state, refresh the commit ref to the current
        // value so a canceled step can't commit a stale `lastChangedValueRef` left over from an
        // earlier change (mirrors the button path).
        if (!hadManualInput) {
          lastChangedValueRef.current = valueRef.current;
        }
        changed = incrementValue(amount, {
          direction: event.key === 'ArrowUp' ? 1 : -1,
          currentValue,
          event: nativeEvent,
          reason: keyboard
        });
      } else if (willSetHome) {
        allowInputSyncRef.current = true;
        changed = setValue(min, createChangeEventDetails(keyboard, nativeEvent));
      } else if (willSetEnd) {
        allowInputSyncRef.current = true;
        changed = setValue(max, createChangeEventDetails(keyboard, nativeEvent));
      }
      if (changed) {
        onValueCommitted(lastChangedValueRef.current ?? valueRef.current, commitDetails);
      }
    },
    onPaste(event) {
      if (event.defaultPrevented || readOnly || disabled) {
        return;
      }
      let pastedData = '';
      try {
        pastedData = event.clipboardData?.getData('text/plain') ?? '';
      } catch {
        if (process.env.NODE_ENV !== 'production') {
          const ownerStackMessage = SafeReact.captureOwnerStack?.() || '';
          warn('<NumberField.Input> could not read clipboard text during paste handling.', ownerStackMessage);
        }
        return;
      }

      // Prevent `onChange` from being called.
      event.preventDefault();

      // Insert the pasted text at the caret/selection instead of replacing the entire value,
      // matching native input behavior (e.g. pasting "5" into "123|" yields "1235").
      const input = event.currentTarget;
      const selectionStart = input.selectionStart ?? inputValue.length;
      const selectionEnd = input.selectionEnd ?? inputValue.length;
      const nextText = inputValue.slice(0, selectionStart) + pastedData + inputValue.slice(selectionEnd);
      const parsedValue = parseNumber(nextText, locale, formatOptionsRef.current);
      if (parsedValue !== null) {
        allowInputSyncRef.current = false;
        pendingCaretRef.current = selectionStart + pastedData.length;
        setValue(parsedValue, createChangeEventDetails(inputPaste, event.nativeEvent));
        setInputValue(nextText);
      }
    }
  };
  const element = useRenderElement('input', componentProps, {
    ref: [forwardedRef, inputRef],
    state,
    props: [inputProps, elementProps, props => validation.getValidationProps(disabled, props)],
    stateAttributesMapping
  });
  return element;
});
if (process.env.NODE_ENV !== "production") NumberFieldInput.displayName = "NumberFieldInput";

export { NumberFieldRoot as N, NumberFieldGroup as a, NumberFieldInput as b, NumberFieldIncrement as c, NumberFieldDecrement as d, stateAttributesMapping as s, useNumberFieldRootContext as u };
