{
  "name": "mask-input",
  "type": "registry:ui",
  "dependencies": [
    "radix-ui"
  ],
  "files": [
    {
      "path": "ui/mask-input.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { Slot as SlotPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\n\nimport { useComposedRefs } from \"@/lib/compose-refs\";\nimport { cn } from \"@/lib/utils\";\n\nconst PAST_YEARS_LIMIT = 120;\nconst FUTURE_YEARS_LIMIT = 10;\nconst DEFAULT_CURRENCY = \"USD\";\nconst DEFAULT_LOCALE = \"en-US\";\n\nconst NUMERIC_MASK_PATTERNS =\n  /^(phone|zipCode|zipCodeExtended|ssn|ein|time|date|creditCard|creditCardExpiry)$/;\nconst CURRENCY_PERCENTAGE_SYMBOLS = /[€$%]/;\n\ninterface CurrencySymbols {\n  currency: string;\n  decimal: string;\n  group: string;\n}\n\nconst formattersCache = new Map<string, Intl.NumberFormat>();\nconst currencyAtEndCache = new Map<string, boolean>();\nconst currencySymbolsCache = new Map<string, CurrencySymbols>();\nconst daysInMonthCache = [\n  31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31,\n] as const;\n\nconst REGEX_CACHE = {\n  digitsOnly: /^\\d+$/,\n  nonDigits: /\\D/g,\n  nonAlphaNumeric: /[^A-Z0-9]/gi,\n  nonNumericDot: /[^0-9.]/g,\n  nonCurrencyChars: /[^\\d.,]/g,\n  hashPattern: /#/g,\n  currencyAtEnd: /\\d\\s*[^\\d\\s]+$/,\n  percentageChars: /[^\\d.]/g,\n  phone: /^\\d{10}$/,\n  ssn: /^\\d{9}$/,\n  zipCode: /^\\d{5}$/,\n  zipCodeExtended: /^\\d{9}$/,\n  isbn: /^\\d{13}$/,\n  ein: /^\\d{9}$/,\n  time: /^\\d{4}$/,\n  creditCard: /^\\d{13,19}$/,\n  creditCardExpiry: /^\\d{4}$/,\n  licensePlate: /^[A-Z0-9]{6}$/,\n  macAddress: /^[A-F0-9]{12}$/,\n  currencyValidation: /^\\d+(\\.\\d{1,2})?$/,\n  ipv4Segment: /^\\d{1,3}$/,\n} as const;\n\nfunction getCachedFormatter(\n  locale: string | undefined,\n  opts: Intl.NumberFormatOptions,\n): Intl.NumberFormat {\n  const {\n    currency,\n    minimumFractionDigits = 0,\n    maximumFractionDigits = 2,\n  } = opts;\n\n  const key = `${locale}|${currency}|${minimumFractionDigits}|${maximumFractionDigits}`;\n\n  if (!formattersCache.has(key)) {\n    try {\n      formattersCache.set(\n        key,\n        new Intl.NumberFormat(locale, {\n          style: \"currency\",\n          currency,\n          ...opts,\n        }),\n      );\n    } catch {\n      formattersCache.set(\n        key,\n        new Intl.NumberFormat(DEFAULT_LOCALE, {\n          style: \"currency\",\n          currency: DEFAULT_CURRENCY,\n          ...opts,\n        }),\n      );\n    }\n  }\n\n  const formatter = formattersCache.get(key);\n  if (!formatter) {\n    throw new Error(`Failed to create formatter for ${key}`);\n  }\n  return formatter;\n}\n\nfunction getCachedCurrencySymbols(opts: TransformOptions): CurrencySymbols {\n  const { locale, currency } = opts;\n\n  const key = `${locale}|${currency}`;\n  const cached = currencySymbolsCache.get(key);\n  if (cached) {\n    return cached;\n  }\n\n  let currencySymbol = \"$\";\n  let decimalSeparator = \".\";\n  let groupSeparator = \",\";\n\n  try {\n    const formatter = getCachedFormatter(locale, {\n      currency,\n      minimumFractionDigits: 0,\n      maximumFractionDigits: 2,\n    });\n    const parts = formatter.formatToParts(1234.5);\n    const currencyPart = parts.find((part) => part.type === \"currency\");\n    const decimalPart = parts.find((part) => part.type === \"decimal\");\n    const groupPart = parts.find((part) => part.type === \"group\");\n\n    if (currencyPart) currencySymbol = currencyPart.value;\n    if (decimalPart) decimalSeparator = decimalPart.value;\n    if (groupPart) groupSeparator = groupPart.value;\n  } catch {\n    // Keep defaults\n  }\n\n  const symbols: CurrencySymbols = {\n    currency: currencySymbol,\n    decimal: decimalSeparator,\n    group: groupSeparator,\n  };\n  currencySymbolsCache.set(key, symbols);\n  return symbols;\n}\n\nfunction isCurrencyAtEnd(opts: TransformOptions): boolean {\n  const { locale, currency } = opts;\n\n  const key = `${locale}|${currency}`;\n  const cached = currencyAtEndCache.get(key);\n  if (cached !== undefined) {\n    return cached;\n  }\n\n  try {\n    const formatter = getCachedFormatter(locale, {\n      currency,\n      minimumFractionDigits: 0,\n      maximumFractionDigits: 0,\n    });\n    const sample = formatter.format(123);\n    const result = REGEX_CACHE.currencyAtEnd.test(sample);\n    currencyAtEndCache.set(key, result);\n    return result;\n  } catch {\n    currencyAtEndCache.set(key, false);\n    return false;\n  }\n}\n\nfunction isCurrencyMask(opts: {\n  mask: MaskPatternKey | MaskPattern | undefined;\n  pattern?: string;\n}): boolean {\n  const { mask, pattern } = opts;\n\n  return (\n    mask === \"currency\" ||\n    Boolean(pattern && (pattern.includes(\"$\") || pattern.includes(\"€\")))\n  );\n}\n\ninterface TransformOptions {\n  currency?: string;\n  locale?: string;\n}\n\ninterface ValidateOptions {\n  min?: number;\n  max?: number;\n}\n\ninterface MaskPattern {\n  pattern: string;\n  transform?: (value: string, opts?: TransformOptions) => string;\n  validate?: (value: string, opts?: ValidateOptions) => boolean;\n}\n\ntype MaskPatternKey =\n  | \"phone\"\n  | \"ssn\"\n  | \"date\"\n  | \"time\"\n  | \"creditCard\"\n  | \"creditCardExpiry\"\n  | \"zipCode\"\n  | \"zipCodeExtended\"\n  | \"currency\"\n  | \"percentage\"\n  | \"licensePlate\"\n  | \"ipv4\"\n  | \"macAddress\"\n  | \"isbn\"\n  | \"ein\";\n\nconst MASK_PATTERNS: Record<MaskPatternKey, MaskPattern> = {\n  phone: {\n    pattern: \"(###) ###-####\",\n    transform: (value) => value.replace(REGEX_CACHE.nonDigits, \"\"),\n    validate: (value) =>\n      REGEX_CACHE.phone.test(value.replace(REGEX_CACHE.nonDigits, \"\")),\n  },\n  ssn: {\n    pattern: \"###-##-####\",\n    transform: (value) => value.replace(REGEX_CACHE.nonDigits, \"\"),\n    validate: (value) =>\n      REGEX_CACHE.ssn.test(value.replace(REGEX_CACHE.nonDigits, \"\")),\n  },\n  date: {\n    pattern: \"##/##/####\",\n    transform: (value) => value.replace(REGEX_CACHE.nonDigits, \"\"),\n    validate: (value) => {\n      const cleaned = value.replace(REGEX_CACHE.nonDigits, \"\");\n      if (cleaned.length !== 8) return false;\n      const month = parseInt(cleaned.substring(0, 2), 10);\n      const day = parseInt(cleaned.substring(2, 4), 10);\n      const year = parseInt(cleaned.substring(4, 8), 10);\n\n      const currentYear = new Date().getFullYear();\n      const minYear = currentYear - PAST_YEARS_LIMIT;\n      const maxYear = currentYear + FUTURE_YEARS_LIMIT;\n      if (\n        month < 1 ||\n        month > 12 ||\n        day < 1 ||\n        year < minYear ||\n        year > maxYear\n      )\n        return false;\n\n      const maxDays =\n        month === 2 &&\n        ((year % 4 === 0 && year % 100 !== 0) || year % 400 === 0)\n          ? 29\n          : (daysInMonthCache[month - 1] ?? 31);\n\n      return day <= maxDays;\n    },\n  },\n  time: {\n    pattern: \"##:##\",\n    transform: (value) => value.replace(REGEX_CACHE.nonDigits, \"\"),\n    validate: (value) => {\n      const cleaned = value.replace(REGEX_CACHE.nonDigits, \"\");\n      if (!REGEX_CACHE.time.test(cleaned)) return false;\n      const hours = parseInt(cleaned.substring(0, 2), 10);\n      const minutes = parseInt(cleaned.substring(2, 4), 10);\n      return hours <= 23 && minutes <= 59;\n    },\n  },\n  creditCard: {\n    pattern: \"#### #### #### ####\",\n    transform: (value) => value.replace(REGEX_CACHE.nonDigits, \"\"),\n    validate: (value) => {\n      const cleaned = value.replace(REGEX_CACHE.nonDigits, \"\");\n      if (!REGEX_CACHE.creditCard.test(cleaned)) return false;\n\n      let sum = 0;\n      let isEven = false;\n      for (let i = cleaned.length - 1; i >= 0; i--) {\n        const digitChar = cleaned[i];\n        if (!digitChar) continue;\n        let digit = parseInt(digitChar, 10);\n        if (isEven) {\n          digit *= 2;\n          if (digit > 9) {\n            digit -= 9;\n          }\n        }\n        sum += digit;\n        isEven = !isEven;\n      }\n      return sum % 10 === 0;\n    },\n  },\n  creditCardExpiry: {\n    pattern: \"##/##\",\n    transform: (value) => value.replace(REGEX_CACHE.nonDigits, \"\"),\n    validate: (value) => {\n      const cleaned = value.replace(REGEX_CACHE.nonDigits, \"\");\n      if (!REGEX_CACHE.creditCardExpiry.test(cleaned)) return false;\n\n      const month = parseInt(cleaned.substring(0, 2), 10);\n      const year = parseInt(cleaned.substring(2, 4), 10);\n\n      if (month < 1 || month > 12) return false;\n\n      const now = new Date();\n      const currentYear = now.getFullYear();\n      const currentMonth = now.getMonth() + 1;\n\n      const fullYear = year <= 75 ? 2000 + year : 1900 + year;\n\n      if (\n        fullYear < currentYear ||\n        (fullYear === currentYear && month < currentMonth)\n      ) {\n        return false;\n      }\n\n      const maxYear = currentYear + 50;\n      if (fullYear > maxYear) {\n        return false;\n      }\n\n      return true;\n    },\n  },\n  zipCode: {\n    pattern: \"#####\",\n    transform: (value) => value.replace(REGEX_CACHE.nonDigits, \"\"),\n    validate: (value) =>\n      REGEX_CACHE.zipCode.test(value.replace(REGEX_CACHE.nonDigits, \"\")),\n  },\n  zipCodeExtended: {\n    pattern: \"#####-####\",\n    transform: (value) => value.replace(REGEX_CACHE.nonDigits, \"\"),\n    validate: (value) =>\n      REGEX_CACHE.zipCodeExtended.test(\n        value.replace(REGEX_CACHE.nonDigits, \"\"),\n      ),\n  },\n  currency: {\n    pattern: \"$###,###.##\",\n    transform: (\n      value,\n      { currency = DEFAULT_CURRENCY, locale = DEFAULT_LOCALE } = {},\n    ) => {\n      let localeDecimalSeparator = \".\";\n\n      try {\n        const formatter = getCachedFormatter(locale, {\n          currency,\n          minimumFractionDigits: 0,\n          maximumFractionDigits: 2,\n        });\n        const parts = formatter.formatToParts(1234.5);\n        const decimalPart = parts.find((part) => part.type === \"decimal\");\n\n        if (decimalPart) localeDecimalSeparator = decimalPart.value;\n      } catch {\n        // Keep defaults\n      }\n\n      const cleaned = value.replace(REGEX_CACHE.nonCurrencyChars, \"\");\n\n      const dotIndex = cleaned.indexOf(\".\");\n      const commaIndex = cleaned.indexOf(\",\");\n\n      let hasDecimalSeparator = false;\n      let decimalIndex = -1;\n\n      if (localeDecimalSeparator === \",\") {\n        const lastCommaIndex = cleaned.lastIndexOf(\",\");\n        if (lastCommaIndex !== -1) {\n          const afterComma = cleaned.substring(lastCommaIndex + 1);\n          if (afterComma.length <= 2 && /^\\d*$/.test(afterComma)) {\n            hasDecimalSeparator = true;\n            decimalIndex = lastCommaIndex;\n          }\n        }\n\n        if (!hasDecimalSeparator && dotIndex !== -1) {\n          const afterDot = cleaned.substring(dotIndex + 1);\n          if (afterDot.length <= 2 && /^\\d*$/.test(afterDot)) {\n            hasDecimalSeparator = true;\n            decimalIndex = dotIndex;\n          }\n        }\n\n        if (!hasDecimalSeparator && cleaned.length >= 4) {\n          const match = cleaned.match(/^(\\d+)\\.(\\d{3})(\\d{1,2})$/);\n          if (match) {\n            const [, beforeDot, thousandsPart, decimalPart] = match;\n            const integerPart = (beforeDot ?? \"\") + (thousandsPart ?? \"\");\n            const result = `${integerPart}.${decimalPart}`;\n            return result;\n          }\n        }\n      } else {\n        const lastDotIndex = cleaned.lastIndexOf(\".\");\n        if (lastDotIndex !== -1) {\n          const afterDot = cleaned.substring(lastDotIndex + 1);\n          if (afterDot.length <= 2 && /^\\d*$/.test(afterDot)) {\n            hasDecimalSeparator = true;\n            decimalIndex = lastDotIndex;\n          }\n        }\n\n        if (!hasDecimalSeparator && commaIndex !== -1) {\n          const afterComma = cleaned.substring(commaIndex + 1);\n          const looksLikeThousands = commaIndex <= 3 && afterComma.length >= 3;\n          if (\n            !looksLikeThousands &&\n            afterComma.length <= 2 &&\n            /^\\d*$/.test(afterComma)\n          ) {\n            hasDecimalSeparator = true;\n            decimalIndex = commaIndex;\n          }\n        }\n      }\n\n      if (hasDecimalSeparator && decimalIndex !== -1) {\n        const beforeDecimal = cleaned\n          .substring(0, decimalIndex)\n          .replace(/[.,]/g, \"\");\n        const afterDecimal = cleaned\n          .substring(decimalIndex + 1)\n          .replace(/[.,]/g, \"\");\n\n        if (afterDecimal === \"\") {\n          const result = `${beforeDecimal}.`;\n          return result;\n        }\n\n        const result = `${beforeDecimal}.${afterDecimal.substring(0, 2)}`;\n        return result;\n      }\n\n      const digitsOnly = cleaned.replace(/[.,]/g, \"\");\n      return digitsOnly;\n    },\n    validate: (value) => {\n      if (!REGEX_CACHE.currencyValidation.test(value)) return false;\n      const num = parseFloat(value);\n      return !Number.isNaN(num) && num >= 0;\n    },\n  },\n  percentage: {\n    pattern: \"##.##%\",\n    transform: (value) => {\n      const cleaned = value.replace(REGEX_CACHE.percentageChars, \"\");\n      const parts = cleaned.split(\".\");\n      if (parts.length > 2) {\n        return `${parts[0]}.${parts.slice(1).join(\"\")}`;\n      }\n      if (parts[1] && parts[1].length > 2) {\n        return `${parts[0]}.${parts[1].substring(0, 2)}`;\n      }\n      return cleaned;\n    },\n    validate: (value, opts = {}) => {\n      const num = parseFloat(value);\n      const min = opts.min ?? 0;\n      const max = opts.max ?? 100;\n      return !Number.isNaN(num) && num >= min && num <= max;\n    },\n  },\n  licensePlate: {\n    pattern: \"###-###\",\n    transform: (value) =>\n      value.replace(REGEX_CACHE.nonAlphaNumeric, \"\").toUpperCase(),\n    validate: (value) => REGEX_CACHE.licensePlate.test(value),\n  },\n  ipv4: {\n    pattern: \"###.###.###.###\",\n    transform: (value) => value.replace(REGEX_CACHE.nonNumericDot, \"\"),\n    validate: (value) => {\n      if (value.includes(\".\")) {\n        const segments = value.split(\".\");\n        if (segments.length > 4) return false;\n\n        return segments.every((segment) => {\n          if (segment === \"\") return true;\n          if (!REGEX_CACHE.ipv4Segment.test(segment)) return false;\n          const num = parseInt(segment, 10);\n          return num <= 255;\n        });\n      } else {\n        if (!REGEX_CACHE.digitsOnly.test(value)) return false;\n        if (value.length > 12) return false;\n\n        const chunks = [];\n        for (let i = 0; i < value.length; i += 3) {\n          chunks.push(value.substring(i, i + 3));\n        }\n\n        if (chunks.length > 4) return false;\n\n        return chunks.every((chunk) => {\n          const num = parseInt(chunk, 10);\n          return num >= 0 && num <= 255;\n        });\n      }\n    },\n  },\n  macAddress: {\n    pattern: \"##:##:##:##:##:##\",\n    transform: (value) =>\n      value.replace(REGEX_CACHE.nonAlphaNumeric, \"\").toUpperCase(),\n    validate: (value) => REGEX_CACHE.macAddress.test(value),\n  },\n  isbn: {\n    pattern: \"###-#-###-#####-#\",\n    transform: (value) => value.replace(REGEX_CACHE.nonDigits, \"\"),\n    validate: (value) =>\n      REGEX_CACHE.isbn.test(value.replace(REGEX_CACHE.nonDigits, \"\")),\n  },\n  ein: {\n    pattern: \"##-#######\",\n    transform: (value) => value.replace(REGEX_CACHE.nonDigits, \"\"),\n    validate: (value) =>\n      REGEX_CACHE.ein.test(value.replace(REGEX_CACHE.nonDigits, \"\")),\n  },\n};\n\nfunction applyMask(opts: {\n  value: string;\n  pattern: string;\n  currency?: string;\n  locale?: string;\n  mask?: MaskPatternKey | MaskPattern;\n}): string {\n  const { value, pattern, currency, locale, mask } = opts;\n\n  const cleanValue = value;\n\n  if (pattern.includes(\"$\") || pattern.includes(\"€\") || mask === \"currency\") {\n    return applyCurrencyMask({\n      value: cleanValue,\n      currency: currency ?? DEFAULT_CURRENCY,\n      locale: locale ?? DEFAULT_LOCALE,\n    });\n  }\n\n  if (pattern.includes(\"%\")) {\n    return applyPercentageMask(cleanValue);\n  }\n\n  if (mask === \"ipv4\") {\n    return cleanValue;\n  }\n\n  const maskedChars: string[] = [];\n  let valueIndex = 0;\n\n  for (let i = 0; i < pattern.length && valueIndex < cleanValue.length; i++) {\n    const patternChar = pattern[i];\n    const valueChar = cleanValue[valueIndex];\n\n    if (patternChar === \"#\" && valueChar) {\n      maskedChars.push(valueChar);\n      valueIndex++;\n    } else if (patternChar) {\n      maskedChars.push(patternChar);\n    }\n  }\n\n  return maskedChars.join(\"\");\n}\n\nfunction applyCurrencyMask(opts: {\n  value: string;\n  currency?: string;\n  locale?: string;\n}): string {\n  const { value, currency = DEFAULT_CURRENCY, locale = DEFAULT_LOCALE } = opts;\n\n  if (!value) return \"\";\n\n  const {\n    currency: currencySymbol,\n    decimal: decimalSeparator,\n    group: groupSeparator,\n  } = getCachedCurrencySymbols({ locale, currency });\n\n  const normalizedValue = value\n    .replace(\n      new RegExp(\n        `\\\\${groupSeparator.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\")}`,\n        \"g\",\n      ),\n      \"\",\n    )\n    .replace(decimalSeparator, \".\");\n\n  const parts = normalizedValue.split(\".\");\n  const integerPart = parts[0] ?? \"\";\n  const fractionalPart = parts[1] ?? \"\";\n\n  if (!integerPart && !fractionalPart) return \"\";\n\n  const intValue = integerPart ?? \"0\";\n  const fracValue = fractionalPart.slice(0, 2);\n\n  const num = Number(`${intValue}.${fracValue ?? \"\"}`);\n\n  if (Number.isNaN(num)) {\n    const cleanedDigits = value.replace(/[^\\d]/g, \"\");\n    if (!cleanedDigits) return \"\";\n    return `${currencySymbol}${cleanedDigits}`;\n  }\n\n  const hasExplicitDecimal =\n    value.includes(\".\") || value.includes(decimalSeparator);\n\n  try {\n    const formatter = getCachedFormatter(locale, {\n      currency,\n      minimumFractionDigits: fracValue ? fracValue.length : 0,\n      maximumFractionDigits: 2,\n    });\n    const result = formatter.format(num);\n\n    if (hasExplicitDecimal && !fracValue) {\n      if (result.match(/^[^\\d\\s]+/)) {\n        const finalResult = result.replace(/(\\d)$/, `$1${decimalSeparator}`);\n        return finalResult;\n      } else {\n        const finalResult = result.replace(\n          /(\\d)(\\s*)([^\\d\\s]+)$/,\n          `$1${decimalSeparator}$2$3`,\n        );\n        return finalResult;\n      }\n    }\n\n    return result;\n  } catch {\n    const formattedInt = intValue.replace(\n      /\\B(?=(\\d{3})+(?!\\d))/g,\n      groupSeparator,\n    );\n    let result = `${currencySymbol}${formattedInt}`;\n    if (hasExplicitDecimal) {\n      result += `${decimalSeparator}${fracValue}`;\n    }\n\n    return result;\n  }\n}\n\nfunction applyPercentageMask(value: string): string {\n  if (!value) return \"\";\n\n  const parts = value.split(\".\");\n  let result = parts[0] ?? \"0\";\n\n  if (value.includes(\".\")) {\n    result += `.${(parts[1] ?? \"\").substring(0, 2)}`;\n  }\n\n  return `${result}%`;\n}\n\nfunction getUnmaskedValue(opts: {\n  value: string;\n  currency?: string;\n  locale?: string;\n  transform?: (value: string, opts?: TransformOptions) => string;\n}): string {\n  const { value, transform, currency, locale } = opts;\n\n  return transform\n    ? transform(value, { currency, locale })\n    : value.replace(REGEX_CACHE.nonDigits, \"\");\n}\n\nfunction toUnmaskedIndex(opts: {\n  masked: string;\n  pattern: string;\n  caret: number;\n}): number {\n  const { masked, pattern, caret } = opts;\n\n  let idx = 0;\n  for (let i = 0; i < caret && i < masked.length && i < pattern.length; i++) {\n    if (pattern[i] === \"#\") {\n      idx++;\n    }\n  }\n\n  return idx;\n}\n\nfunction fromUnmaskedIndex(opts: {\n  masked: string;\n  pattern: string;\n  unmaskedIndex: number;\n}): number {\n  const { masked, pattern, unmaskedIndex } = opts;\n\n  let seen = 0;\n  for (let i = 0; i < masked.length && i < pattern.length; i++) {\n    if (pattern[i] === \"#\") {\n      seen++;\n      if (seen === unmaskedIndex) {\n        return i + 1;\n      }\n    }\n  }\n\n  return masked.length;\n}\n\nfunction getCurrencyCaretPosition(opts: {\n  newValue: string;\n  mask: MaskPatternKey | MaskPattern | undefined;\n  transformOpts: TransformOptions;\n  oldCursorPosition?: number;\n  oldValue?: string;\n  previousUnmasked?: string;\n}): number {\n  const {\n    newValue,\n    mask,\n    transformOpts,\n    oldCursorPosition,\n    oldValue,\n    previousUnmasked,\n  } = opts;\n\n  if (\n    oldCursorPosition !== undefined &&\n    oldValue &&\n    previousUnmasked !== undefined\n  ) {\n    if (oldCursorPosition < oldValue.length) {\n      const digitsBeforeCursor = oldValue\n        .substring(0, oldCursorPosition)\n        .replace(/\\D/g, \"\").length;\n\n      let digitCount = 0;\n      for (let i = 0; i < newValue.length; i++) {\n        if (/\\d/.test(newValue[i] ?? \"\")) {\n          digitCount++;\n          if (digitCount === digitsBeforeCursor) {\n            return i + 1;\n          }\n        }\n      }\n    }\n  }\n\n  if (mask === \"currency\") {\n    const currencyAtEnd = isCurrencyAtEnd(transformOpts);\n    if (currencyAtEnd) {\n      const match = newValue.match(/(\\d)\\s*([^\\d\\s]+)$/);\n      if (match?.[1]) {\n        return newValue.lastIndexOf(match[1]) + 1;\n      } else {\n        return newValue.length;\n      }\n    } else {\n      return newValue.length;\n    }\n  } else {\n    return newValue.length;\n  }\n}\n\nfunction getPatternCaretPosition(opts: {\n  newValue: string;\n  maskPattern: MaskPattern;\n  currentUnmasked: string;\n  oldCursorPosition?: number;\n  oldValue?: string;\n  previousUnmasked?: string;\n}): number {\n  const {\n    newValue,\n    maskPattern,\n    currentUnmasked,\n    oldCursorPosition,\n    oldValue,\n    previousUnmasked,\n  } = opts;\n  let position = 0;\n  let unmaskedCount = 0;\n\n  if (\n    oldCursorPosition !== undefined &&\n    oldValue &&\n    previousUnmasked !== undefined\n  ) {\n    const oldUnmaskedIndex = toUnmaskedIndex({\n      masked: oldValue,\n      pattern: maskPattern.pattern,\n      caret: oldCursorPosition,\n    });\n\n    if (oldCursorPosition < oldValue.length) {\n      const targetUnmaskedIndex = Math.min(\n        oldUnmaskedIndex,\n        currentUnmasked.length,\n      );\n\n      for (\n        let i = 0;\n        i < maskPattern.pattern.length && i < newValue.length;\n        i++\n      ) {\n        if (maskPattern.pattern[i] === \"#\") {\n          unmaskedCount++;\n          if (unmaskedCount <= targetUnmaskedIndex) {\n            position = i + 1;\n          }\n        }\n      }\n\n      return position;\n    }\n  }\n\n  for (let i = 0; i < maskPattern.pattern.length && i < newValue.length; i++) {\n    if (maskPattern.pattern[i] === \"#\") {\n      unmaskedCount++;\n      if (unmaskedCount <= currentUnmasked.length) {\n        position = i + 1;\n      }\n    }\n  }\n\n  return position;\n}\n\ntype InputElement = React.ComponentRef<\"input\">;\n\ninterface MaskInputProps extends React.ComponentProps<\"input\"> {\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (maskedValue: string, unmaskedValue: string) => void;\n  onValidate?: (isValid: boolean, unmaskedValue: string) => void;\n  validationMode?: \"onChange\" | \"onBlur\" | \"onSubmit\" | \"onTouched\" | \"all\";\n  mask?: MaskPatternKey | MaskPattern;\n  maskPlaceholder?: string;\n  currency?: string;\n  locale?: string;\n  asChild?: boolean;\n  invalid?: boolean;\n  withoutMask?: boolean;\n}\n\nfunction MaskInput(props: MaskInputProps) {\n  const {\n    value: valueProp,\n    defaultValue,\n    onValueChange: onValueChangeProp,\n    onValidate,\n    onBlur: onBlurProp,\n    onFocus: onFocusProp,\n    onKeyDown: onKeyDownProp,\n    onPaste: onPasteProp,\n    onCompositionStart: onCompositionStartProp,\n    onCompositionEnd: onCompositionEndProp,\n    validationMode = \"onChange\",\n    mask,\n    maskPlaceholder,\n    currency = DEFAULT_CURRENCY,\n    locale = DEFAULT_LOCALE,\n    placeholder,\n    inputMode,\n    min,\n    max,\n    maxLength,\n    asChild = false,\n    disabled = false,\n    invalid = false,\n    readOnly = false,\n    required = false,\n    withoutMask = false,\n    className,\n    ref,\n    ...inputProps\n  } = props;\n\n  const [internalValue, setInternalValue] = React.useState(defaultValue ?? \"\");\n  const [focused, setFocused] = React.useState(false);\n  const [composing, setComposing] = React.useState(false);\n  const [touched, setTouched] = React.useState(false);\n  const inputRef = React.useRef<HTMLInputElement>(null);\n  const composedRef = useComposedRefs(ref, inputRef);\n\n  const isControlled = valueProp !== undefined;\n  const value = isControlled ? valueProp : internalValue;\n\n  const maskPattern = React.useMemo(() => {\n    if (typeof mask === \"string\") {\n      return MASK_PATTERNS[mask];\n    }\n    return mask;\n  }, [mask]);\n\n  const transformOpts = React.useMemo(\n    () => ({\n      currency,\n      locale,\n    }),\n    [currency, locale],\n  );\n\n  const placeholderValue = React.useMemo(() => {\n    if (withoutMask) return placeholder;\n\n    if (placeholder && maskPlaceholder) {\n      return focused ? maskPlaceholder : placeholder;\n    }\n\n    if (maskPlaceholder) {\n      return focused ? maskPlaceholder : undefined;\n    }\n\n    return placeholder;\n  }, [placeholder, maskPlaceholder, focused, withoutMask]);\n\n  const displayValue = React.useMemo(() => {\n    if (withoutMask || !maskPattern || !value) return value ?? \"\";\n    const unmasked = getUnmaskedValue({\n      value,\n      transform: maskPattern.transform,\n      ...transformOpts,\n    });\n    return applyMask({\n      value: unmasked,\n      pattern: maskPattern.pattern,\n      ...transformOpts,\n      mask,\n    });\n  }, [value, maskPattern, withoutMask, transformOpts, mask]);\n\n  const tokenCount = React.useMemo(() => {\n    if (!maskPattern || CURRENCY_PERCENTAGE_SYMBOLS.test(maskPattern.pattern))\n      return undefined;\n    return maskPattern.pattern.match(REGEX_CACHE.hashPattern)?.length ?? 0;\n  }, [maskPattern]);\n\n  const calculatedMaxLength = tokenCount\n    ? maskPattern?.pattern.length\n    : maxLength;\n\n  const calculatedInputMode = React.useMemo(() => {\n    if (inputMode) return inputMode;\n    if (!maskPattern) return undefined;\n\n    if (mask === \"currency\" || mask === \"percentage\" || mask === \"ipv4\") {\n      return \"decimal\";\n    }\n\n    if (typeof mask === \"string\" && NUMERIC_MASK_PATTERNS.test(mask)) {\n      return \"numeric\";\n    }\n    return undefined;\n  }, [maskPattern, mask, inputMode]);\n\n  const shouldValidate = React.useCallback(\n    (trigger: \"change\" | \"blur\") => {\n      if (!onValidate || !maskPattern?.validate) return false;\n\n      switch (validationMode) {\n        case \"onChange\":\n          return trigger === \"change\";\n        case \"onBlur\":\n          return trigger === \"blur\";\n        case \"onSubmit\":\n          return false;\n        case \"onTouched\":\n          return touched ? trigger === \"change\" : trigger === \"blur\";\n        case \"all\":\n          return true;\n        default:\n          return trigger === \"change\";\n      }\n    },\n    [onValidate, maskPattern, validationMode, touched],\n  );\n\n  const validationOpts = React.useMemo(\n    () => ({\n      min: typeof min === \"string\" ? parseFloat(min) : min,\n      max: typeof max === \"string\" ? parseFloat(max) : max,\n    }),\n    [min, max],\n  );\n\n  const onInputValidate = React.useCallback(\n    (unmaskedValue: string) => {\n      if (onValidate && maskPattern?.validate) {\n        const isValid = maskPattern.validate(unmaskedValue, validationOpts);\n        onValidate(isValid, unmaskedValue);\n      }\n    },\n    [onValidate, maskPattern?.validate, validationOpts],\n  );\n\n  const onValueChange = React.useCallback(\n    (event: React.ChangeEvent<InputElement>) => {\n      const inputValue = event.target.value;\n      let newValue = inputValue;\n      let unmaskedValue = inputValue;\n\n      if (composing) {\n        if (!isControlled) setInternalValue(inputValue);\n        return;\n      }\n\n      if (withoutMask || !maskPattern) {\n        if (!isControlled) setInternalValue(inputValue);\n        if (shouldValidate(\"change\")) onValidate?.(true, inputValue);\n        onValueChangeProp?.(inputValue, inputValue);\n        return;\n      }\n\n      if (maskPattern) {\n        unmaskedValue = getUnmaskedValue({\n          value: inputValue,\n          transform: maskPattern.transform,\n          ...transformOpts,\n        });\n        newValue = applyMask({\n          value: unmaskedValue,\n          pattern: maskPattern.pattern,\n          ...transformOpts,\n          mask,\n        });\n\n        if (inputRef.current && newValue !== inputValue) {\n          const inputElement = inputRef.current;\n          if (!(inputElement instanceof HTMLInputElement)) return;\n\n          const oldCursorPosition = inputElement.selectionStart ?? 0;\n\n          inputElement.value = newValue;\n\n          const currentUnmasked = getUnmaskedValue({\n            value: newValue,\n            transform: maskPattern.transform,\n            ...transformOpts,\n          });\n\n          let newCursorPosition: number;\n\n          const previousUnmasked = getUnmaskedValue({\n            value,\n            transform: maskPattern.transform,\n            ...transformOpts,\n          });\n\n          if (CURRENCY_PERCENTAGE_SYMBOLS.test(maskPattern.pattern)) {\n            newCursorPosition = getCurrencyCaretPosition({\n              newValue,\n              mask,\n              transformOpts,\n              oldCursorPosition,\n              oldValue: inputValue,\n              previousUnmasked,\n            });\n          } else {\n            newCursorPosition = getPatternCaretPosition({\n              newValue,\n              maskPattern,\n              currentUnmasked,\n              oldCursorPosition,\n              oldValue: inputValue,\n              previousUnmasked,\n            });\n          }\n\n          if (isCurrencyMask({ mask, pattern: maskPattern.pattern })) {\n            if (mask === \"currency\") {\n              const currencyAtEnd = isCurrencyAtEnd(transformOpts);\n              if (!currencyAtEnd) {\n                newCursorPosition = Math.max(1, newCursorPosition);\n              }\n            } else {\n              newCursorPosition = Math.max(1, newCursorPosition);\n            }\n          } else if (maskPattern.pattern.includes(\"%\")) {\n            newCursorPosition = Math.min(\n              newValue.length - 1,\n              newCursorPosition,\n            );\n          }\n\n          newCursorPosition = Math.min(newCursorPosition, newValue.length);\n\n          inputElement.setSelectionRange(newCursorPosition, newCursorPosition);\n        }\n      }\n\n      if (!isControlled) {\n        setInternalValue(newValue);\n      }\n\n      if (shouldValidate(\"change\")) {\n        onInputValidate(unmaskedValue);\n      }\n\n      onValueChangeProp?.(newValue, unmaskedValue);\n    },\n    [\n      maskPattern,\n      isControlled,\n      onValueChangeProp,\n      onValidate,\n      onInputValidate,\n      composing,\n      shouldValidate,\n      withoutMask,\n      transformOpts,\n      mask,\n      value,\n    ],\n  );\n\n  const onFocus = React.useCallback(\n    (event: React.FocusEvent<InputElement>) => {\n      onFocusProp?.(event);\n      if (event.defaultPrevented) return;\n\n      setFocused(true);\n    },\n    [onFocusProp],\n  );\n\n  const onBlur = React.useCallback(\n    (event: React.FocusEvent<InputElement>) => {\n      onBlurProp?.(event);\n      if (event.defaultPrevented) return;\n\n      setFocused(false);\n\n      if (!touched) {\n        setTouched(true);\n      }\n\n      if (shouldValidate(\"blur\")) {\n        const currentValue = event.target.value;\n        const unmaskedValue = maskPattern\n          ? getUnmaskedValue({\n              value: currentValue,\n              transform: maskPattern.transform,\n              ...transformOpts,\n            })\n          : currentValue;\n        onInputValidate(unmaskedValue);\n      }\n    },\n    [\n      onBlurProp,\n      touched,\n      shouldValidate,\n      onInputValidate,\n      maskPattern,\n      transformOpts,\n    ],\n  );\n\n  const onCompositionStart = React.useCallback(\n    (event: React.CompositionEvent<InputElement>) => {\n      onCompositionStartProp?.(event);\n      if (event.defaultPrevented) return;\n\n      setComposing(true);\n    },\n    [onCompositionStartProp],\n  );\n\n  const onCompositionEnd = React.useCallback(\n    (event: React.CompositionEvent<InputElement>) => {\n      onCompositionEndProp?.(event);\n      if (event.defaultPrevented) return;\n\n      setComposing(false);\n\n      const inputElement = inputRef.current;\n      if (!inputElement) return;\n      if (!(inputElement instanceof HTMLInputElement)) return;\n      const inputValue = inputElement.value;\n\n      if (!maskPattern || withoutMask) {\n        if (!isControlled) setInternalValue(inputValue);\n        if (shouldValidate(\"change\")) onValidate?.(true, inputValue);\n        onValueChangeProp?.(inputValue, inputValue);\n        return;\n      }\n\n      const unmasked = getUnmaskedValue({\n        value: inputValue,\n        transform: maskPattern.transform,\n        ...transformOpts,\n      });\n      const masked = applyMask({\n        value: unmasked,\n        pattern: maskPattern.pattern,\n        ...transformOpts,\n        mask,\n      });\n\n      if (!isControlled) setInternalValue(masked);\n      if (shouldValidate(\"change\")) onInputValidate(unmasked);\n      onValueChangeProp?.(masked, unmasked);\n    },\n    [\n      onCompositionEndProp,\n      maskPattern,\n      withoutMask,\n      isControlled,\n      shouldValidate,\n      onValidate,\n      onValueChangeProp,\n      transformOpts,\n      mask,\n      onInputValidate,\n    ],\n  );\n\n  const onPaste = React.useCallback(\n    (event: React.ClipboardEvent<InputElement>) => {\n      onPasteProp?.(event);\n      if (event.defaultPrevented) return;\n\n      if (withoutMask || !maskPattern) return;\n\n      if (mask === \"ipv4\") return;\n\n      const target = event.target as InputElement;\n      if (!(target instanceof HTMLInputElement)) return;\n\n      const pastedData = event.clipboardData.getData(\"text\");\n      if (!pastedData) return;\n\n      event.preventDefault();\n\n      const currentValue = target.value;\n      const selectionStart = target.selectionStart ?? 0;\n      const selectionEnd = target.selectionEnd ?? 0;\n\n      const beforeSelection = currentValue.slice(0, selectionStart);\n      const afterSelection = currentValue.slice(selectionEnd);\n      const newInputValue = beforeSelection + pastedData + afterSelection;\n\n      const unmasked = getUnmaskedValue({\n        value: newInputValue,\n        transform: maskPattern.transform,\n        ...transformOpts,\n      });\n      const newMaskedValue = applyMask({\n        value: unmasked,\n        pattern: maskPattern.pattern,\n        ...transformOpts,\n        mask,\n      });\n\n      target.value = newMaskedValue;\n\n      if (isCurrencyMask({ mask, pattern: maskPattern.pattern })) {\n        const currencyAtEnd = isCurrencyAtEnd(transformOpts);\n        const caret = currencyAtEnd\n          ? newMaskedValue.search(/\\s*[^\\d\\s]+$/)\n          : newMaskedValue.length;\n        target.setSelectionRange(caret, caret);\n        return;\n      }\n\n      if (maskPattern.pattern.includes(\"%\")) {\n        target.setSelectionRange(\n          newMaskedValue.length - 1,\n          newMaskedValue.length - 1,\n        );\n        return;\n      }\n\n      let newCursorPosition = newMaskedValue.length;\n      try {\n        const unmaskedCount = unmasked.length;\n        let position = 0;\n        let count = 0;\n\n        for (\n          let i = 0;\n          i < maskPattern.pattern.length && i < newMaskedValue.length;\n          i++\n        ) {\n          if (maskPattern.pattern[i] === \"#\") {\n            count++;\n            if (count <= unmaskedCount) {\n              position = i + 1;\n            }\n          }\n        }\n        newCursorPosition = position;\n      } catch {\n        // fallback to end\n      }\n\n      target.setSelectionRange(newCursorPosition, newCursorPosition);\n\n      if (!isControlled) setInternalValue(newMaskedValue);\n      if (shouldValidate(\"change\")) onInputValidate(unmasked);\n      onValueChangeProp?.(newMaskedValue, unmasked);\n    },\n    [\n      onPasteProp,\n      withoutMask,\n      maskPattern,\n      mask,\n      transformOpts,\n      isControlled,\n      shouldValidate,\n      onInputValidate,\n      onValueChangeProp,\n    ],\n  );\n\n  const onKeyDown = React.useCallback(\n    (event: React.KeyboardEvent<InputElement>) => {\n      onKeyDownProp?.(event);\n      if (event.defaultPrevented) return;\n\n      if (withoutMask || !maskPattern) return;\n\n      if (mask === \"ipv4\") return;\n\n      if (event.key === \"Backspace\") {\n        const target = event.target as InputElement;\n        if (!(target instanceof HTMLInputElement)) return;\n        const cursorPosition = target.selectionStart ?? 0;\n        const selectionEnd = target.selectionEnd ?? 0;\n        const currentValue = target.value;\n\n        if (\n          mask === \"currency\" ||\n          mask === \"percentage\" ||\n          maskPattern.pattern.includes(\"$\") ||\n          maskPattern.pattern.includes(\"€\") ||\n          maskPattern.pattern.includes(\"%\")\n        ) {\n          return;\n        }\n\n        if (cursorPosition !== selectionEnd) {\n          return;\n        }\n\n        if (cursorPosition > 0) {\n          const charBeforeCursor = currentValue[cursorPosition - 1];\n\n          if (charBeforeCursor) {\n            event.preventDefault();\n\n            const unmaskedIndex = toUnmaskedIndex({\n              masked: currentValue,\n              pattern: maskPattern.pattern,\n              caret: cursorPosition,\n            });\n\n            if (unmaskedIndex > 0) {\n              const currentUnmasked = getUnmaskedValue({\n                value: currentValue,\n                transform: maskPattern.transform,\n                ...transformOpts,\n              });\n              const nextUnmasked =\n                currentUnmasked.slice(0, unmaskedIndex - 1) +\n                currentUnmasked.slice(unmaskedIndex);\n              const nextMasked = applyMask({\n                value: nextUnmasked,\n                pattern: maskPattern.pattern,\n                ...transformOpts,\n                mask,\n              });\n\n              target.value = nextMasked;\n              const nextCaret = fromUnmaskedIndex({\n                masked: nextMasked,\n                pattern: maskPattern.pattern,\n                unmaskedIndex: unmaskedIndex - 1,\n              });\n\n              target.setSelectionRange(nextCaret, nextCaret);\n\n              onValueChangeProp?.(nextMasked, nextUnmasked);\n            }\n            return;\n          }\n        }\n      }\n\n      if (event.key === \"Delete\") {\n        const target = event.target as InputElement;\n        if (!(target instanceof HTMLInputElement)) return;\n        const cursorPosition = target.selectionStart ?? 0;\n        const selectionEnd = target.selectionEnd ?? 0;\n        const currentValue = target.value;\n\n        if (\n          mask === \"currency\" ||\n          mask === \"percentage\" ||\n          maskPattern.pattern.includes(\"$\") ||\n          maskPattern.pattern.includes(\"€\") ||\n          maskPattern.pattern.includes(\"%\")\n        ) {\n          return;\n        }\n\n        if (cursorPosition !== selectionEnd) {\n          return;\n        }\n\n        if (cursorPosition < currentValue.length) {\n          const charAtCursor = currentValue[cursorPosition];\n\n          if (charAtCursor) {\n            event.preventDefault();\n\n            const unmaskedIndex = toUnmaskedIndex({\n              masked: currentValue,\n              pattern: maskPattern.pattern,\n              caret: cursorPosition,\n            });\n\n            const currentUnmasked = getUnmaskedValue({\n              value: currentValue,\n              transform: maskPattern.transform,\n              ...transformOpts,\n            });\n\n            if (unmaskedIndex < currentUnmasked.length) {\n              const nextUnmasked =\n                currentUnmasked.slice(0, unmaskedIndex) +\n                currentUnmasked.slice(unmaskedIndex + 1);\n              const nextMasked = applyMask({\n                value: nextUnmasked,\n                pattern: maskPattern.pattern,\n                ...transformOpts,\n                mask,\n              });\n\n              target.value = nextMasked;\n              const nextCaret = fromUnmaskedIndex({\n                masked: nextMasked,\n                pattern: maskPattern.pattern,\n                unmaskedIndex: unmaskedIndex,\n              });\n\n              target.setSelectionRange(nextCaret, nextCaret);\n\n              onValueChangeProp?.(nextMasked, nextUnmasked);\n            }\n            return;\n          }\n        }\n      }\n    },\n    [\n      maskPattern,\n      onKeyDownProp,\n      onValueChangeProp,\n      transformOpts,\n      mask,\n      withoutMask,\n    ],\n  );\n\n  const InputPrimitive = asChild ? SlotPrimitive.Slot : \"input\";\n\n  return (\n    <InputPrimitive\n      aria-invalid={invalid}\n      data-disabled={disabled ? \"\" : undefined}\n      data-invalid={invalid ? \"\" : undefined}\n      data-readonly={readOnly ? \"\" : undefined}\n      data-required={required ? \"\" : undefined}\n      data-slot=\"mask-input\"\n      {...inputProps}\n      className={cn(\n        \"flex 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 file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-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      placeholder={placeholderValue}\n      ref={composedRef}\n      value={displayValue}\n      disabled={disabled}\n      maxLength={calculatedMaxLength}\n      readOnly={readOnly}\n      required={required}\n      inputMode={calculatedInputMode}\n      min={min}\n      max={max}\n      onFocus={onFocus}\n      onBlur={onBlur}\n      onKeyDown={onKeyDown}\n      onPaste={onPaste}\n      onChange={onValueChange}\n      onCompositionStart={onCompositionStart}\n      onCompositionEnd={onCompositionEnd}\n    />\n  );\n}\n\nexport {\n  applyCurrencyMask,\n  applyMask,\n  applyPercentageMask,\n  fromUnmaskedIndex,\n  getUnmaskedValue,\n  MASK_PATTERNS,\n  MaskInput,\n  type MaskInputProps,\n  type MaskPattern,\n  toUnmaskedIndex,\n};\n",
      "target": ""
    },
    {
      "path": "lib/compose-refs.ts",
      "type": "registry:lib",
      "content": "/**\n * @see https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/compose-refs.tsx\n */\n\nimport * as React from \"react\";\n\ntype PossibleRef<T> = React.Ref<T> | undefined;\n\n/**\n * Set a given ref to a given value\n * This utility takes care of different types of refs: callback refs and RefObject(s)\n */\nfunction setRef<T>(ref: PossibleRef<T>, value: T) {\n  if (typeof ref === \"function\") {\n    return ref(value);\n  }\n\n  if (ref !== null && ref !== undefined) {\n    ref.current = value;\n  }\n}\n\n/**\n * A utility to compose multiple refs together\n * Accepts callback refs and RefObject(s)\n */\nfunction composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n  return (node) => {\n    let hasCleanup = false;\n    const cleanups = refs.map((ref) => {\n      const cleanup = setRef(ref, node);\n      if (!hasCleanup && typeof cleanup === \"function\") {\n        hasCleanup = true;\n      }\n      return cleanup;\n    });\n\n    // React <19 will log an error to the console if a callback ref returns a\n    // value. We don't use ref cleanups internally so this will only happen if a\n    // user's ref callback returns a value, which we only expect if they are\n    // using the cleanup functionality added in React 19.\n    if (hasCleanup) {\n      return () => {\n        for (let i = 0; i < cleanups.length; i++) {\n          const cleanup = cleanups[i];\n          if (typeof cleanup === \"function\") {\n            cleanup();\n          } else {\n            setRef(refs[i], null);\n          }\n        }\n      };\n    }\n  };\n}\n\n/**\n * A custom hook that composes multiple refs\n * Accepts callback refs and RefObject(s)\n */\nfunction useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {\n  // oxlint-disable-next-line react/exhaustive-deps -- we want to memoize by all values\n  return React.useCallback(composeRefs(...refs), refs);\n}\n\nexport { composeRefs, useComposedRefs };\n",
      "target": ""
    }
  ]
}