{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "masked-input",
  "title": "Masked Input",
  "description": "Pattern-masked inputs and localized currency formatting with stable caret positioning.",
  "dependencies": [],
  "registryDependencies": [
    "utils",
    "@drift-ui/use-controllable-state"
  ],
  "files": [
    {
      "path": "registry/default/ui/masked-input.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { useControllableState } from \"@/registry/default/hooks/use-controllable-state\"\nimport {\n  applyMask,\n  calculateCurrencyCaretPosition,\n  calculateMaskCaretPosition,\n  DEFAULT_MASK_DEFINITIONS,\n  deleteCurrencyDigitPastFormatting,\n  deleteTokenPastFormatting,\n  formatCurrency,\n  formatCurrencyLive,\n  parseCurrency,\n  unmaskValue,\n} from \"@/registry/default/lib/masked-input\"\nimport { cn } from \"@/registry/default/lib/utils\"\n\ninterface MaskedInputProps extends Omit<\n  React.ComponentProps<\"input\">,\n  \"value\" | \"defaultValue\" | \"onChange\"\n> {\n  mask: string\n  value?: string\n  defaultValue?: string\n  onValueChange?: (value: string, formattedValue: string) => void\n  definitions?: Record<string, RegExp>\n}\n\nfunction MaskedInput({\n  mask,\n  value,\n  defaultValue = \"\",\n  onValueChange,\n  definitions = DEFAULT_MASK_DEFINITIONS,\n  name,\n  disabled,\n  className,\n  ref,\n  ...props\n}: MaskedInputProps) {\n  const { value: rawValue, setValue: setRawValue } = useControllableState({\n    value,\n    defaultValue,\n    onChange: (val) => {\n      const { formatted } = applyMask(val, mask, definitions)\n      onValueChange?.(val, formatted)\n    },\n  })\n\n  const inputRef = React.useRef<HTMLInputElement | null>(null)\n  React.useImperativeHandle(ref, () => inputRef.current as HTMLInputElement)\n\n  const [edit, setEdit] = React.useState<{\n    text: string\n    caret: number\n  } | null>(null)\n\n  const formattedValue = React.useMemo(\n    () => applyMask(rawValue ?? \"\", mask, definitions).formatted,\n    [rawValue, mask, definitions]\n  )\n\n  const display =\n    edit !== null &&\n    unmaskValue(edit.text, mask, definitions) === (rawValue ?? \"\")\n      ? edit.text\n      : formattedValue\n\n  React.useLayoutEffect(() => {\n    const input = inputRef.current\n    if (input && document.activeElement === input && edit !== null) {\n      input.setSelectionRange(edit.caret, edit.caret)\n    }\n  }, [edit])\n\n  function handleChange(event: React.ChangeEvent<HTMLInputElement>) {\n    const { value: rawInput, selectionStart } = event.target\n    const { inputType } = (event.nativeEvent as InputEvent) || {}\n    const isForward = inputType === \"deleteContentForward\"\n\n    const { text, caret } = deleteTokenPastFormatting(\n      display,\n      rawInput,\n      selectionStart ?? rawInput.length,\n      isForward,\n      mask,\n      definitions\n    )\n\n    const unmasked = unmaskValue(text, mask, definitions)\n    const { formatted: nextFormatted, raw: nextRaw } = applyMask(\n      unmasked,\n      mask,\n      definitions\n    )\n\n    const nextCursor = calculateMaskCaretPosition({\n      nextFormatted,\n      rawInput: text,\n      rawCursorPos: caret,\n      mask,\n      definitions,\n    })\n\n    setEdit({ text: nextFormatted, caret: nextCursor })\n    setRawValue(nextRaw)\n  }\n\n  return (\n    <>\n      <input\n        {...props}\n        ref={inputRef}\n        type=\"text\"\n        data-slot=\"masked-input\"\n        disabled={disabled}\n        value={display}\n        onChange={handleChange}\n        className={cn(\n          \"h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30\",\n          \"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n          \"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40\",\n          className\n        )}\n      />\n      {name && (\n        <input\n          type=\"hidden\"\n          name={name}\n          disabled={disabled}\n          value={rawValue ?? \"\"}\n        />\n      )}\n    </>\n  )\n}\n\ninterface CurrencyInputProps extends Omit<\n  React.ComponentProps<\"input\">,\n  \"value\" | \"defaultValue\" | \"onChange\"\n> {\n  currency?: string\n  locale?: string\n  value?: number | null\n  defaultValue?: number | null\n  onValueChange?: (value: number | null, formattedValue: string) => void\n  min?: number\n  max?: number\n}\n\nfunction CurrencyInput({\n  currency = \"USD\",\n  locale = \"en-US\",\n  value,\n  defaultValue = null,\n  onValueChange,\n  min,\n  max,\n  name,\n  disabled,\n  className,\n  onBlur,\n  ref,\n  ...props\n}: CurrencyInputProps) {\n  const { value: centsValue, setValue: setCentsValue } = useControllableState<\n    number | null\n  >({\n    value,\n    defaultValue,\n    onChange: (val) => {\n      const formatted = formatCurrency(val, locale, currency)\n      onValueChange?.(val, formatted)\n    },\n  })\n\n  const inputRef = React.useRef<HTMLInputElement | null>(null)\n  React.useImperativeHandle(ref, () => inputRef.current as HTMLInputElement)\n\n  const [edit, setEdit] = React.useState<{\n    text: string\n    caret: number\n  } | null>(null)\n\n  const display =\n    edit !== null && parseCurrency(edit.text, locale, currency) === centsValue\n      ? edit.text\n      : centsValue !== null && centsValue !== undefined\n        ? formatCurrency(centsValue, locale, currency)\n        : \"\"\n\n  React.useLayoutEffect(() => {\n    const input = inputRef.current\n    if (input && document.activeElement === input && edit !== null) {\n      input.setSelectionRange(edit.caret, edit.caret)\n    }\n  }, [edit])\n\n  function handleChange(event: React.ChangeEvent<HTMLInputElement>) {\n    const { value: rawInput, selectionStart } = event.target\n    const { inputType } = (event.nativeEvent as InputEvent) || {}\n    const isForward = inputType === \"deleteContentForward\"\n\n    const { text, caret } = deleteCurrencyDigitPastFormatting(\n      display,\n      rawInput,\n      selectionStart ?? rawInput.length,\n      isForward,\n      locale\n    )\n\n    if (!text.trim()) {\n      setEdit(null)\n      setCentsValue(null)\n      return\n    }\n\n    const { formatted, cents } = formatCurrencyLive(text, locale, currency)\n\n    let finalCents = cents\n    let finalFormatted = formatted\n\n    if (finalCents !== null && max !== undefined && finalCents > max) {\n      finalCents = max\n      finalFormatted = formatCurrency(max, locale, currency)\n    }\n\n    const nextCursor = calculateCurrencyCaretPosition({\n      nextFormatted: finalFormatted,\n      rawInput: text,\n      rawCursorPos: caret,\n      locale,\n      currency,\n    })\n\n    setEdit({ text: finalFormatted, caret: nextCursor })\n    setCentsValue(finalCents)\n  }\n\n  function handleBlur(event: React.FocusEvent<HTMLInputElement>) {\n    setEdit(null)\n    if (centsValue !== null) {\n      const clamped = Math.min(\n        max ?? Infinity,\n        Math.max(min ?? -Infinity, centsValue)\n      )\n      if (clamped !== centsValue) {\n        setCentsValue(clamped)\n      }\n    }\n    onBlur?.(event)\n  }\n\n  return (\n    <>\n      <input\n        {...props}\n        ref={inputRef}\n        type=\"text\"\n        inputMode=\"decimal\"\n        data-slot=\"currency-input\"\n        disabled={disabled}\n        value={display}\n        onChange={handleChange}\n        onBlur={handleBlur}\n        className={cn(\n          \"h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30\",\n          \"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n          \"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40\",\n          className\n        )}\n      />\n      {name && (\n        <input\n          type=\"hidden\"\n          name={name}\n          disabled={disabled}\n          value={\n            centsValue !== null && centsValue !== undefined ? centsValue : \"\"\n          }\n        />\n      )}\n    </>\n  )\n}\n\nexport {\n  MaskedInput,\n  CurrencyInput,\n  type MaskedInputProps,\n  type CurrencyInputProps,\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/default/lib/masked-input.ts",
      "content": "export const DEFAULT_MASK_DEFINITIONS: Record<string, RegExp> = {\n  \"#\": /\\d/,\n  a: /[a-zA-Z]/,\n  A: /[a-zA-Z]/,\n  \"*\": /[a-zA-Z0-9]/,\n}\n\nexport function isMaskToken(\n  char: string,\n  definitions = DEFAULT_MASK_DEFINITIONS\n): boolean {\n  return char in definitions\n}\n\nexport interface UnmaskResult {\n  raw: string\n  tokensBeforeCursor: number\n}\n\n/**\n * Unmasks a value against a mask pattern, returning the raw unmasked tokens\n * and tracking how many accepted tokens occurred strictly before cursorPos.\n */\nexport function unmaskWithDetails(\n  value: string,\n  mask: string,\n  cursorPos?: number,\n  definitions = DEFAULT_MASK_DEFINITIONS\n): UnmaskResult {\n  if (!value) return { raw: \"\", tokensBeforeCursor: 0 }\n\n  let raw = \"\"\n  let tokensBeforeCursor = 0\n  let valueIdx = 0\n  let maskIdx = 0\n\n  while (maskIdx < mask.length && valueIdx < value.length) {\n    const maskChar = mask[maskIdx]\n    const tokenRegex = definitions[maskChar]\n\n    if (tokenRegex) {\n      let found = false\n      while (valueIdx < value.length) {\n        const char = value[valueIdx]\n        const currentIndex = valueIdx\n        valueIdx++\n        if (tokenRegex.test(char)) {\n          raw += char\n          if (cursorPos !== undefined && currentIndex < cursorPos) {\n            tokensBeforeCursor++\n          }\n          found = true\n          break\n        }\n      }\n      if (!found) break\n      maskIdx++\n    } else {\n      let literalSegment = \"\"\n      let lookahead = maskIdx\n      while (lookahead < mask.length && !definitions[mask[lookahead]]) {\n        literalSegment += mask[lookahead]\n        lookahead++\n      }\n\n      if (value[valueIdx] === literalSegment[0]) {\n        let matched = 0\n        while (\n          matched < literalSegment.length &&\n          valueIdx < value.length &&\n          value[valueIdx] === literalSegment[matched]\n        ) {\n          matched++\n          valueIdx++\n        }\n      }\n      maskIdx = lookahead\n    }\n  }\n\n  return { raw, tokensBeforeCursor }\n}\n\nexport function unmaskValue(\n  value: string,\n  mask: string,\n  definitions = DEFAULT_MASK_DEFINITIONS\n): string {\n  return unmaskWithDetails(value, mask, undefined, definitions).raw\n}\n\nexport function applyMask(\n  raw: string,\n  mask: string,\n  definitions = DEFAULT_MASK_DEFINITIONS\n): { formatted: string; raw: string } {\n  if (!raw) return { formatted: \"\", raw: \"\" }\n\n  let formatted = \"\"\n  let cleanRaw = \"\"\n  let rawIdx = 0\n\n  for (let m = 0; m < mask.length; m++) {\n    const maskChar = mask[m]\n    const tokenRegex = definitions[maskChar]\n\n    if (tokenRegex) {\n      if (rawIdx >= raw.length) break\n      let found = false\n      while (rawIdx < raw.length) {\n        const char = raw[rawIdx++]\n        if (tokenRegex.test(char)) {\n          formatted += char\n          cleanRaw += char\n          found = true\n          break\n        }\n      }\n      if (!found) break\n    } else {\n      if (rawIdx > 0 && rawIdx <= raw.length) {\n        formatted += maskChar\n      } else if (rawIdx === 0 && raw.length > 0) {\n        formatted += maskChar\n      } else {\n        break\n      }\n    }\n  }\n\n  return { formatted, raw: cleanRaw }\n}\n\nexport function deleteTokenPastFormatting(\n  previous: string,\n  text: string,\n  caret: number,\n  isForward: boolean,\n  mask: string,\n  definitions = DEFAULT_MASK_DEFINITIONS\n): { text: string; caret: number } {\n  const previousRaw = unmaskValue(previous, mask, definitions)\n  const textRaw = unmaskValue(text, mask, definitions)\n  const removedFormatting =\n    previous.length - text.length === 1 && previousRaw === textRaw\n\n  if (!removedFormatting) return { text, caret }\n\n  if (isForward) {\n    for (let i = caret; i < text.length; i++) {\n      const char = text[i]\n      if (Object.values(definitions).some((regex) => regex.test(char))) {\n        return {\n          text: text.slice(0, i) + text.slice(i + 1),\n          caret,\n        }\n      }\n    }\n  } else {\n    const rawBefore = unmaskValue(text.slice(0, caret), mask, definitions)\n    if (rawBefore.length === 0) return { text, caret }\n\n    for (let i = caret - 1; i >= 0; i--) {\n      const char = text[i]\n      if (Object.values(definitions).some((regex) => regex.test(char))) {\n        return {\n          text: text.slice(0, i) + text.slice(i + 1),\n          caret: i,\n        }\n      }\n    }\n  }\n\n  return { text, caret }\n}\n\nexport function calculateMaskCaretPosition({\n  nextFormatted,\n  rawInput,\n  rawCursorPos,\n  mask,\n  definitions = DEFAULT_MASK_DEFINITIONS,\n}: {\n  nextFormatted: string\n  rawInput: string\n  rawCursorPos: number\n  mask: string\n  definitions?: Record<string, RegExp>\n}): number {\n  const { tokensBeforeCursor } = unmaskWithDetails(\n    rawInput,\n    mask,\n    rawCursorPos,\n    definitions\n  )\n\n  if (tokensBeforeCursor === 0) {\n    let idx = 0\n    while (idx < nextFormatted.length && !isMaskToken(mask[idx], definitions)) {\n      idx++\n    }\n    return idx\n  }\n\n  let tokenCount = 0\n  let caretIndex = nextFormatted.length\n\n  for (let i = 0; i < nextFormatted.length; i++) {\n    if (isMaskToken(mask[i], definitions)) {\n      tokenCount++\n      if (tokenCount === tokensBeforeCursor) {\n        caretIndex = i + 1\n        break\n      }\n    }\n  }\n\n  while (\n    caretIndex < nextFormatted.length &&\n    !isMaskToken(mask[caretIndex], definitions)\n  ) {\n    caretIndex++\n  }\n\n  return Math.min(caretIndex, nextFormatted.length)\n}\n\nexport function getLocaleDigitMap(locale: string = \"en-US\"): {\n  toStandard: Record<string, string>\n  isDigit: (char: string) => boolean\n} {\n  const formatter = new Intl.NumberFormat(locale, { useGrouping: false })\n  const toStandard: Record<string, string> = {}\n  for (let i = 0; i <= 9; i++) {\n    const loc = formatter.format(i)\n    toStandard[loc] = String(i)\n    toStandard[String(i)] = String(i)\n  }\n  return {\n    toStandard,\n    isDigit: (char: string) => char in toStandard,\n  }\n}\n\nexport function getCurrencyInfo(\n  locale: string = \"en-US\",\n  currency: string = \"USD\"\n) {\n  const formatter = new Intl.NumberFormat(locale, {\n    style: \"currency\",\n    currency,\n  })\n  const fractionDigits = formatter.resolvedOptions().maximumFractionDigits ?? 2\n  const sampleParts = formatter.formatToParts(123456.78)\n\n  const decimal = sampleParts.find((p) => p.type === \"decimal\")?.value ?? \".\"\n  const group = sampleParts.find((p) => p.type === \"group\")?.value ?? \",\"\n  const currencySymbol =\n    sampleParts.find((p) => p.type === \"currency\")?.value ?? currency\n\n  const firstNumIdx = sampleParts.findIndex((p) => p.type === \"integer\")\n  const lastNumIdx = sampleParts.findLastIndex(\n    (p) => p.type === \"fraction\" || p.type === \"integer\"\n  )\n\n  const prefix = sampleParts\n    .slice(0, firstNumIdx !== -1 ? firstNumIdx : 0)\n    .map((p) => p.value)\n    .join(\"\")\n  const suffix = sampleParts\n    .slice(lastNumIdx !== -1 ? lastNumIdx + 1 : sampleParts.length)\n    .map((p) => p.value)\n    .join(\"\")\n\n  const negParts = formatter.formatToParts(-123456.78)\n  const negFirstNumIdx = negParts.findIndex((p) => p.type === \"integer\")\n  const negLastNumIdx = negParts.findLastIndex(\n    (p) => p.type === \"fraction\" || p.type === \"integer\"\n  )\n\n  const negPrefix = negParts\n    .slice(0, negFirstNumIdx !== -1 ? negFirstNumIdx : 0)\n    .map((p) => p.value)\n    .join(\"\")\n  const negSuffix = negParts\n    .slice(negLastNumIdx !== -1 ? negLastNumIdx + 1 : negParts.length)\n    .map((p) => p.value)\n    .join(\"\")\n\n  return {\n    formatter,\n    fractionDigits,\n    decimal,\n    group,\n    currencySymbol,\n    prefix,\n    suffix,\n    negPrefix,\n    negSuffix,\n  }\n}\n\nexport function formatCurrency(\n  cents: number | null | undefined,\n  locale: string = \"en-US\",\n  currency: string = \"USD\"\n): string {\n  if (cents === null || cents === undefined || Number.isNaN(cents)) {\n    return \"\"\n  }\n\n  const { formatter, fractionDigits } = getCurrencyInfo(locale, currency)\n  const amount =\n    fractionDigits > 0 ? cents / Math.pow(10, fractionDigits) : cents\n\n  return formatter.format(amount)\n}\n\nexport function parseCurrency(\n  text: string,\n  locale: string = \"en-US\",\n  currency: string = \"USD\"\n): number | null {\n  if (!text || !text.trim()) return null\n\n  const { fractionDigits, decimal } = getCurrencyInfo(locale, currency)\n  const { toStandard, isDigit } = getLocaleDigitMap(locale)\n\n  let isNegative = false\n  let intDigits = \"\"\n  let fracDigits = \"\"\n  let foundDecimal = false\n\n  for (let i = 0; i < text.length; i++) {\n    const char = text[i]\n    if (char === \"-\") {\n      isNegative = true\n    } else if (char === decimal && fractionDigits > 0) {\n      foundDecimal = true\n    } else if (isDigit(char)) {\n      const standardChar = toStandard[char]\n      if (foundDecimal) {\n        if (fracDigits.length < fractionDigits) {\n          fracDigits += standardChar\n        }\n      } else {\n        const maxIntDigits = Math.max(1, 15 - fractionDigits)\n        if (intDigits.length < maxIntDigits) {\n          intDigits += standardChar\n        }\n      }\n    }\n  }\n\n  if (!intDigits && !fracDigits) return null\n\n  const intVal = intDigits ? Number(intDigits) : 0\n  const paddedFrac = fracDigits\n    .padEnd(fractionDigits, \"0\")\n    .slice(0, fractionDigits)\n  const fracVal = fractionDigits > 0 ? Number(paddedFrac) : 0\n  const total = intVal * Math.pow(10, fractionDigits) + fracVal\n\n  return isNegative ? -total : total\n}\n\nexport function formatCurrencyLive(\n  input: string,\n  locale: string = \"en-US\",\n  currency: string = \"USD\"\n): { formatted: string; cents: number | null } {\n  if (!input || !input.trim()) {\n    return { formatted: \"\", cents: null }\n  }\n\n  const { fractionDigits, decimal, prefix, suffix, negPrefix, negSuffix } =\n    getCurrencyInfo(locale, currency)\n  const { toStandard, isDigit } = getLocaleDigitMap(locale)\n\n  let isNegative = false\n  let intDigits = \"\"\n  let fracDigits = \"\"\n  let foundDecimal = false\n\n  for (let i = 0; i < input.length; i++) {\n    const char = input[i]\n    if (char === \"-\") {\n      isNegative = true\n    } else if (char === decimal && fractionDigits > 0) {\n      if (!foundDecimal) {\n        foundDecimal = true\n      }\n    } else if (isDigit(char)) {\n      const standardChar = toStandard[char]\n      if (foundDecimal) {\n        if (fracDigits.length < fractionDigits) {\n          fracDigits += standardChar\n        }\n      } else {\n        const maxIntDigits = Math.max(1, 15 - fractionDigits)\n        if (intDigits.length < maxIntDigits) {\n          intDigits += standardChar\n        }\n      }\n    }\n  }\n\n  if (!intDigits && !fracDigits && !foundDecimal) {\n    return { formatted: \"\", cents: null }\n  }\n\n  const intNumber = intDigits ? Number(intDigits) : 0\n  const formattedInt = new Intl.NumberFormat(locale, {\n    useGrouping: true,\n  }).format(intNumber)\n\n  const displayInt = intDigits ? formattedInt : \"0\"\n\n  let body = displayInt\n  if (foundDecimal) {\n    let displayFrac = \"\"\n    const locFormatter = new Intl.NumberFormat(locale, { useGrouping: false })\n    for (const c of fracDigits) {\n      displayFrac += locFormatter.format(Number(c))\n    }\n    body += decimal + displayFrac\n  }\n\n  const activePrefix = isNegative ? negPrefix : prefix\n  const activeSuffix = isNegative ? negSuffix : suffix\n  const formatted = `${activePrefix}${body}${activeSuffix}`\n\n  const paddedFrac = fracDigits\n    .padEnd(fractionDigits, \"0\")\n    .slice(0, fractionDigits)\n  const fracVal = fractionDigits > 0 ? Number(paddedFrac) : 0\n  const total = intNumber * Math.pow(10, fractionDigits) + fracVal\n  const cents = isNegative ? -total : total\n\n  return { formatted, cents }\n}\n\nexport function deleteCurrencyDigitPastFormatting(\n  previous: string,\n  text: string,\n  caret: number,\n  isForward: boolean,\n  locale: string = \"en-US\"\n): { text: string; caret: number } {\n  const { isDigit } = getLocaleDigitMap(locale)\n\n  const prevDigits = Array.from(previous).filter(isDigit).join(\"\")\n  const textDigits = Array.from(text).filter(isDigit).join(\"\")\n\n  const removedFormatting =\n    previous.length - text.length === 1 && prevDigits === textDigits\n\n  if (!removedFormatting) return { text, caret }\n\n  if (isForward) {\n    for (let i = caret; i < text.length; i++) {\n      if (isDigit(text[i])) {\n        return {\n          text: text.slice(0, i) + text.slice(i + 1),\n          caret,\n        }\n      }\n    }\n  } else {\n    for (let i = caret - 1; i >= 0; i--) {\n      if (isDigit(text[i])) {\n        return {\n          text: text.slice(0, i) + text.slice(i + 1),\n          caret: i,\n        }\n      }\n    }\n  }\n\n  return { text, caret }\n}\n\nexport function calculateCurrencyCaretPosition({\n  nextFormatted,\n  rawInput,\n  rawCursorPos,\n  locale = \"en-US\",\n  currency = \"USD\",\n}: {\n  nextFormatted: string\n  rawInput: string\n  rawCursorPos: number\n  locale?: string\n  currency?: string\n}): number {\n  const { decimal } = getCurrencyInfo(locale, currency)\n  const { isDigit } = getLocaleDigitMap(locale)\n\n  const rawDecimalIdx = rawInput.indexOf(decimal)\n  const isPastDecimal = rawDecimalIdx !== -1 && rawCursorPos > rawDecimalIdx\n\n  let digitsBefore = 0\n  for (let i = 0; i < Math.min(rawCursorPos, rawInput.length); i++) {\n    if (isDigit(rawInput[i])) {\n      digitsBefore++\n    }\n  }\n\n  if (digitsBefore === 0) {\n    if (isPastDecimal) {\n      const nextDec = nextFormatted.indexOf(decimal)\n      return nextDec !== -1 ? nextDec + 1 : nextFormatted.length\n    }\n    let idx = 0\n    while (idx < nextFormatted.length && !isDigit(nextFormatted[idx])) {\n      idx++\n    }\n    return idx\n  }\n\n  let count = 0\n  let caretIndex = nextFormatted.length\n\n  for (let i = 0; i < nextFormatted.length; i++) {\n    if (isDigit(nextFormatted[i])) {\n      count++\n      if (count === digitsBefore) {\n        caretIndex = i + 1\n        break\n      }\n    }\n  }\n\n  if (isPastDecimal) {\n    const nextDec = nextFormatted.indexOf(decimal)\n    if (nextDec !== -1 && caretIndex <= nextDec) {\n      caretIndex = nextDec + 1\n    }\n  }\n\n  return Math.min(caretIndex, nextFormatted.length)\n}\n",
      "type": "registry:lib"
    }
  ],
  "type": "registry:ui"
}