{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "masked-input",
  "title": "Masked Input",
  "description": "Holds a text field to one fixed shape while it is typed — a postal or ZIP code, a product or licence key, a serial, model or part number, an employee, member, account or policy number, a VIN, an IBAN, a SWIFT/BIC, a tracking number, an ISBN, a tax or national ID, a MAC address, a coupon or voucher code. You give it a mask: # is a digit, A a letter, * either, \\ escapes the next character, and everything else is a separator the field writes for the person instead of asking them to type it. Common asks it answers: 'react input mask', 'masked input react', 'input mask shadcn', 'format input as you type react', 'react-input-mask alternative', 'cleave.js react', 'imask react', 'cursor jumps to end react input', 'caret jumps to end when formatting input', 'input formatting moves the cursor', 'react controlled input cursor position', 'cannot edit middle of formatted input', 'paste into masked input duplicates the dashes', 'backspace does nothing in masked input', 'strip formatting before submit react', 'store unformatted value react input', 'postal code input react', 'zip code mask', 'product key input component', 'license key input four groups', 'serial number input mask', 'VIN input react', 'IBAN input formatting', 'uppercase input react', 'numeric keyboard on mobile for code field'. shadcn/ui has nothing here at all: maskInput, inputMask, formatMask, placeholderChar, setSelectionRange and selectionStart each appear exactly zero times across the 223KB of component source its registry serves today (62 of its 63 indexed items are actually served; questionnaire is listed but 404s on both style tracks), and its single mention of a caret is input-otp's animate-caret-blink, a decorative blinking div rather than a caret position. So this gets hand-written every time, and the hand-written version has the same three bugs. The caret is the first and the worst: reformatting on every keystroke replaces the whole value, so the caret goes to the end — which nobody notices while typing a fresh code left to right, and which makes the field unusable the moment someone goes back to fix the third character, because every keypress throws them to the end again. It is tracked here in typed characters rather than string offsets, since the separators move underneath it — typing the fourth digit of ###-#### inserts two characters where one was typed. Backspace and Delete are intercepted for the same reason: left to the browser, backspacing over a separator deletes it, the reformat puts it straight back, and the key looks broken, so those keys remove a typed character and let the separator follow. Pasting is the second: a field that strips separators and re-inserts them cannot tell one the person pasted from one it is about to add, and produces 123--4567. Here there is no paste handler at all — pasted text, typed text and a value handed over by a parent are read through the mask by one walk, so a separator the text already carries is consumed by the slot that was going to write one, and a character that does not fit its slot is dropped rather than slid sideways into the next slot that would take it, because silently reordering what someone typed is worse than ignoring a keystroke they can see did nothing. What gets submitted is the third: the field shows 123-4567 and reports 1234567, with the formatted string beside it rather than instead of it, so a controlled parent that stores what it is handed stores the value and not the presentation. name submits the raw value, formattedName submits the formatted one when the separators are part of what you store. Not the component for a format another pulld field already owns — date-input, time-input, phone-input, currency-input and otp-input each carry rules a mask cannot express (a month that stops at twelve, a calling code that decides how many digits follow it, a decimal separator that moves with the locale, a box per digit), so use those for dates, times, phone numbers, money and one-time codes, and this one for the shapes that are only a shape. It also makes no claim about whether the contents are real: a mask is a shape and not a checksum — an IBAN has mod-97, a VIN has a check digit, a card has Luhn — so isMaskComplete reports that every slot is filled and stops there, and the field never turns red on the second keystroke. The rest of the API: tokens adds or replaces the slot characters, which is how a VIN excludes I, O and Q that no general table knows about (tokens={{ ...MASK_TOKENS, V: /[A-HJ-NPR-Z0-9]/ }}); transform folds to uppercase or lowercase for keys, VINs and IBANs; onValueChange reports the raw value plus { formatted, complete } on every keystroke; a mask that changes — a postal code whose shape follows the country above it — takes the value with it and tells the parent. Accessibility is where a mask usually fails quietly: ___-____ in a placeholder is read out as underscores or skipped entirely, so describeMask says the shape in words (\"3 digits, then 4 digits\") into an sr-only description, and a caller's own aria-describedby is kept alongside it rather than overwritten; pass hint for another language or null to drop it. inputMode is derived, so an all-digit mask opens the number pad on a phone and a mixed one does not; type is text, so a leading zero survives and the separators can exist at all; autoCapitalize, autoCorrect, autoComplete and spellCheck default off, because a mobile keyboard rewrites a serial number into a word between the keypress and the change event, and all four can be overridden by a field that does have a browser entry. Nothing rendered depends on feature detection, so the server and the first client render agree and there is no hydration mismatch. formatWithMask, unmask, isMaskComplete and describeMask are exported as pure functions for the places the same answer is needed outside React — a confirmation screen, an admin table, a route handler checking what arrived, a migration normalising a column written both ways. Styled with shadcn tokens (input, ring, muted-foreground) for light and dark, className merges rather than fights, focus ring is focus-visible. One file, zero dependencies.",
  "files": [
    {
      "path": "registry/ui/masked-input.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/**\n * What each mask character means, and what may be typed where it stands.\n *\n * Three is the whole default vocabulary, because a mask is a shape and not a validator: `#` for a\n * digit, `A` for a letter, `*` for either. Every other character in a mask is a literal that the\n * field writes for the person rather than asking them to type it — the dash in a postal code, the\n * spaces in an IBAN, the slashes in a part number.\n *\n * None of these carry the `g` flag, and none ever should. A global regular expression keeps a\n * `lastIndex` between calls, so the same pattern tested twice against the same character answers\n * `true` and then `false` — a mask that accepts every other keystroke, which reads as a flaky\n * keyboard rather than as a bug in a table.\n *\n * Pass `tokens` to add to or replace this. A vehicle identification number excludes I, O and Q so\n * they cannot be misread as 1 and 0, and no general table knows that: `tokens={{ V: /[A-HJ-NPR-Z0-9]/ }}`\n * with a mask of seventeen `V`s is the whole of it. Merge rather than replace when you still want\n * the defaults: `tokens={{ ...MASK_TOKENS, V: /[A-HJ-NPR-Z0-9]/ }}`.\n */\nexport const MASK_TOKENS: Readonly<Record<string, RegExp>> = {\n  \"#\": /[0-9]/,\n  A: /[A-Za-z]/,\n  \"*\": /[A-Za-z0-9]/,\n}\n\n/** How a character is folded before it is offered to a slot. */\nexport type MaskTransform = \"none\" | \"uppercase\" | \"lowercase\"\n\n/** One position in a compiled mask: somewhere to type, or a separator the field writes itself. */\nexport type MaskToken =\n  | { kind: \"fill\"; key: string; test: RegExp }\n  | { kind: \"literal\"; char: string }\n\n/** English names for the default tokens, singular and plural, for `describeMask`. */\nconst TOKEN_NAMES: Readonly<Record<string, readonly [string, string]>> = {\n  \"#\": [\"digit\", \"digits\"],\n  A: [\"letter\", \"letters\"],\n  \"*\": [\"letter or digit\", \"letters or digits\"],\n}\n\n/**\n * Turns a mask string into the list of positions it describes.\n *\n * A backslash escapes the character after it, so a mask can contain a literal `#`, `A` or `*` —\n * `\"\\\\#####\"` is a hash followed by four digits. This is the only reason the mask language needs an\n * escape at all, and leaving it out would make those three characters permanently unreachable as\n * separators.\n *\n * A trailing lone backslash is taken literally rather than treated as an error, because a mask is\n * usually being assembled from something a person is still typing into a settings field, and\n * throwing halfway through would take the form down.\n */\nexport function compileMask(\n  mask: string,\n  tokens: Readonly<Record<string, RegExp>> = MASK_TOKENS\n): MaskToken[] {\n  const compiled: MaskToken[] = []\n  for (let i = 0; i < mask.length; i++) {\n    const char = mask[i]\n    if (char === \"\\\\\" && i + 1 < mask.length) {\n      compiled.push({ kind: \"literal\", char: mask[++i] })\n      continue\n    }\n    const test = tokens[char]\n    if (test) compiled.push({ kind: \"fill\", key: char, test })\n    else compiled.push({ kind: \"literal\", char })\n  }\n  return compiled\n}\n\n/** Folds one character the way `transform` asks. */\nfunction fold(char: string, transform: MaskTransform): string {\n  if (transform === \"uppercase\") return char.toUpperCase()\n  if (transform === \"lowercase\") return char.toLowerCase()\n  return char\n}\n\n/**\n * Reads `text` through the mask, keeping the characters that fit a slot and dropping the rest.\n *\n * This one walk is the whole component. Typing a character, pasting a finished string, and being\n * handed a stored value by a parent are the same operation with different text, which is why there\n * is no paste handler here: pasting `123-4567` into `###-####` arrives as an ordinary change event\n * whose text already carries the dash, and the dash is consumed by the literal that was going to be\n * written there anyway. The hand-rolled version of this — strip the separators, then re-insert them\n * — is what doubles them up, because it cannot tell a separator the person pasted from one it is\n * about to add.\n *\n * Three rules, in the order they are tried at each position:\n *\n * - A literal slot consumes the character when it is that literal, and otherwise consumes nothing\n *   and moves on. That second half is what lets someone type `1234567` straight through and get\n *   `123-4567`: the dash is never typed, so the slot writes it.\n * - A fill slot takes the character when the token accepts it, after folding.\n * - Anything else is discarded. A letter typed where a digit goes does not slide sideways into the\n *   next slot that would take it — silently reordering what someone typed is worse than ignoring a\n *   keystroke they can see did nothing.\n *\n * `upTo` is a caret offset into `text`; `rawAtUpTo` comes back as the number of kept characters\n * that lay before it. See `commit` for why the caret has to be counted in this unit.\n */\nfunction readThroughMask(\n  text: string,\n  compiled: MaskToken[],\n  transform: MaskTransform,\n  upTo: number = Number.POSITIVE_INFINITY\n): { raw: string; rawAtUpTo: number } {\n  let raw = \"\"\n  let rawAtUpTo = 0\n  let at = 0\n  let slot = 0\n  while (slot < compiled.length && at < text.length) {\n    const token = compiled[slot]\n    if (token.kind === \"literal\") {\n      if (text[at] === token.char) at++\n      slot++\n      continue\n    }\n    const candidate = fold(text[at], transform)\n    if (token.test.test(candidate)) {\n      raw += candidate\n      at++\n      slot++\n      // `at` now points past the character just taken, so it lay before the caret when `at <= upTo`.\n      if (at <= upTo) rawAtUpTo = raw.length\n    } else {\n      at++\n    }\n  }\n  return { raw, rawAtUpTo }\n}\n\n/**\n * Lays `raw` out across the mask, stopping before any separator that has not been earned.\n *\n * A field holding three digits of `###-####` shows `123`, not `123-`. The eager version reads as a\n * shape hint, but it puts a character on screen that nobody typed and that backspace then has to\n * pretend to delete, and the placeholder carries that hint without either problem.\n */\nfunction layout(raw: string, compiled: MaskToken[]): string {\n  let out = \"\"\n  let at = 0\n  for (const token of compiled) {\n    if (at >= raw.length) break\n    if (token.kind === \"literal\") {\n      out += token.char\n      continue\n    }\n    out += raw[at++]\n  }\n  return out\n}\n\n/** How many characters the mask has room for. */\nfunction fillCount(compiled: MaskToken[]): number {\n  let n = 0\n  for (const token of compiled) if (token.kind === \"fill\") n++\n  return n\n}\n\n/**\n * The offset in a laid-out string just after its `n`th typed character.\n *\n * Counted this way because the separators move. Typing the fourth digit of `###-####` turns `123`\n * into `123-4`, which inserts two characters where one was typed; an offset restored by arithmetic\n * lands in the wrong place and an offset restored by \"put it at the end\" — what a hand-rolled mask\n * does — makes the field impossible to correct in the middle. \"Just after the nth typed character\"\n * survives both.\n */\nfunction caretAfterFill(display: string, n: number, compiled: MaskToken[]): number {\n  if (n <= 0) return 0\n  let seen = 0\n  let at = 0\n  for (const token of compiled) {\n    if (at >= display.length) break\n    at++\n    if (token.kind === \"fill\" && ++seen === n) return at\n  }\n  return display.length\n}\n\n/**\n * The mask applied to any string, for showing a stored value somewhere it is not being edited — a\n * confirmation step, an admin table, a receipt, an export.\n *\n * Total on purpose: it accepts the raw value, the already-formatted value, or something in between\n * a person pasted into a spreadsheet, and gives back the one right rendering. That is what makes it\n * safe to call on a row out of a database without first knowing which form was stored.\n */\nexport function formatWithMask(\n  value: string,\n  mask: string,\n  options: { tokens?: Readonly<Record<string, RegExp>>; transform?: MaskTransform } = {}\n): string {\n  const compiled = compileMask(mask, options.tokens ?? MASK_TOKENS)\n  return layout(readThroughMask(value, compiled, options.transform ?? \"none\").raw, compiled)\n}\n\n/**\n * The separators taken back out — the value to store and to send.\n *\n * The other half of the pair above, and the one a server wants: `unmask(\"123-4567\", \"###-####\")` is\n * `\"1234567\"`. Exported because the same answer is needed outside React — in a route handler\n * checking what arrived, in a migration normalising a column that was written both ways.\n */\nexport function unmask(\n  value: string,\n  mask: string,\n  options: { tokens?: Readonly<Record<string, RegExp>>; transform?: MaskTransform } = {}\n): string {\n  const compiled = compileMask(mask, options.tokens ?? MASK_TOKENS)\n  return readThroughMask(value, compiled, options.transform ?? \"none\").raw\n}\n\n/**\n * Whether the value fills the mask completely.\n *\n * The component reports this with every change rather than acting on it. A field that refuses to\n * blur until it is full, or that turns red on the second keystroke, is a field that fights the\n * person typing into it; whether an incomplete serial number is an error is a question about the\n * form, and it is answered on submit.\n */\nexport function isMaskComplete(\n  value: string,\n  mask: string,\n  options: { tokens?: Readonly<Record<string, RegExp>>; transform?: MaskTransform } = {}\n): boolean {\n  const compiled = compileMask(mask, options.tokens ?? MASK_TOKENS)\n  const total = fillCount(compiled)\n  if (total === 0) return false\n  return readThroughMask(value, compiled, options.transform ?? \"none\").raw.length === total\n}\n\n/**\n * The mask said out loud — `\"###-####\"` becomes `\"3 digits, then 4 digits\"`.\n *\n * A mask is a visual convention, and `___-____` in a placeholder is invisible to someone using a\n * screen reader: it is read as underscores, or skipped, and either way the shape of the thing they\n * are being asked for never arrives. The component wires this into the field's description so it\n * does.\n *\n * English only, and mechanically so. A field in another language should pass its own `hint`; a\n * generated sentence in the wrong language is worse than none, because it sounds authored.\n */\nexport function describeMask(\n  mask: string,\n  options: { tokens?: Readonly<Record<string, RegExp>>; names?: Readonly<Record<string, readonly [string, string]>> } = {}\n): string {\n  const compiled = compileMask(mask, options.tokens ?? MASK_TOKENS)\n  const names = options.names ?? TOKEN_NAMES\n  const runs: { key: string; count: number }[] = []\n  // Runs break on a literal as well as on a change of token, so `##-##` reads as two groups of two\n  // rather than as one group of four — the grouping is the point of saying it at all.\n  let previousWasFill = false\n  for (const token of compiled) {\n    if (token.kind !== \"fill\") {\n      previousWasFill = false\n      continue\n    }\n    const last = runs[runs.length - 1]\n    if (last && previousWasFill && last.key === token.key) last.count++\n    else runs.push({ key: token.key, count: 1 })\n    previousWasFill = true\n  }\n  return runs\n    .map(({ key, count }) => {\n      const [one, many] = names[key] ?? [\"character\", \"characters\"]\n      return `${count} ${count === 1 ? one : many}`\n    })\n    .join(\", then \")\n}\n\n/** The mask with every slot shown as `placeholderChar` — `###-####` becomes `___-____`. */\nfunction maskPlaceholder(compiled: MaskToken[], placeholderChar: string): string {\n  let out = \"\"\n  for (const token of compiled) out += token.kind === \"literal\" ? token.char : placeholderChar\n  return out\n}\n\nexport interface MaskedInputProps\n  extends Omit<\n    React.ComponentPropsWithoutRef<\"input\">,\n    \"value\" | \"defaultValue\" | \"onChange\" | \"type\" | \"pattern\"\n  > {\n  /**\n   * The shape to type into. `#` is a digit, `A` a letter, `*` either, `\\` escapes the next\n   * character, and everything else is a separator the field writes itself: `\"###-####\"`,\n   * `\"AA-####\"`, `\"****-****-****-****\"`.\n   */\n  mask: string\n  /** Extra or replacement slot characters. Spread `MASK_TOKENS` in to keep the three defaults. */\n  tokens?: Readonly<Record<string, RegExp>>\n  /** Folds what is typed — `\"uppercase\"` for product keys, VINs and IBANs (default `\"none\"`). */\n  transform?: MaskTransform\n  /**\n   * Controlled value, with or without separators — both are read through the mask, so a column\n   * written in either form comes back up correctly. Pair with `onValueChange`.\n   */\n  value?: string\n  /** Starting value for an uncontrolled field. Ignored once `value` is passed. */\n  defaultValue?: string\n  /**\n   * Called on every keystroke with the value stripped of separators, plus the formatted string and\n   * whether the mask is now full.\n   *\n   * The stripped value is first because it is the one that should be stored: a mask is a reading\n   * aid for one form on one screen, and a database column that has absorbed it can no longer be\n   * compared, indexed or re-rendered under a different convention.\n   */\n  onValueChange?: (value: string, meta: { formatted: string; complete: boolean }) => void\n  /** Submits the value without separators with a native form. */\n  name?: string\n  /** Submits the formatted value alongside it, when the separators are part of what you store. */\n  formattedName?: string\n  /**\n   * The accessible description of the shape. Defaults to `describeMask(mask)`; pass your own for a\n   * field that is not in English, or `null` when a visible hint of your own already says it.\n   */\n  hint?: string | null\n  /** The character standing in for a slot in the default placeholder (default `\"_\"`). */\n  placeholderChar?: string\n  className?: string\n}\n\n/**\n * A text field that holds one fixed shape — a postal code, a product key, a serial or part number,\n * an employee or account number, a VIN, an IBAN.\n *\n * ```tsx\n * const [key, setKey] = React.useState(\"\")\n *\n * return (\n *   <>\n *     <Label htmlFor=\"key\">Product key</Label>\n *     <MaskedInput\n *       id=\"key\"\n *       name=\"product_key\"\n *       mask=\"****-****-****-****\"\n *       transform=\"uppercase\"\n *       value={key}\n *       onValueChange={setKey}\n *     />\n *   </>\n * )\n * ```\n *\n * Three things go wrong in every hand-rolled version, and they are the three this exists for.\n *\n * The caret is the first and the worst. Reformatting on each keystroke replaces the whole value, so\n * the caret goes to the end — which nobody notices while typing a fresh code left to right, and\n * which makes the field unusable the moment someone goes back to fix the third character. It is\n * tracked here in typed characters rather than offsets, because the separators move underneath it.\n *\n * Pasting is the second. An already-formatted string pasted into a field that strips and re-inserts\n * separators comes out with two of each. Here the pasted text is read through the mask like\n * anything else, and a separator it already carries is consumed by the slot that was going to write\n * one.\n *\n * What gets submitted is the third. The field shows `123-4567` and reports `1234567`, and the\n * formatted string is available beside it rather than instead of it. A controlled parent that\n * stores what it is handed stores the value, not the presentation.\n *\n * Deliberately not here: any judgement about whether the contents are real. A mask is a shape, and\n * a shape is not a checksum — an IBAN has a mod-97 check, a VIN has a check digit, a credit card\n * has Luhn, and each belongs to the form that knows which one it is asking for. `isMaskComplete`\n * says whether every slot is filled and stops there.\n *\n * This is also not the right component for a format some other field here already owns. Dates,\n * times, phone numbers, currency amounts and one-time codes each carry rules a mask cannot express\n * — a month that only goes to twelve, a calling code that decides how many digits follow, a decimal\n * separator that changes with the locale, a box per digit — and `date-input`, `time-input`,\n * `phone-input`, `currency-input` and `otp-input` exist for exactly that reason.\n */\nexport const MaskedInput = React.forwardRef<HTMLInputElement, MaskedInputProps>(\n  function MaskedInput(\n    {\n      mask,\n      tokens = MASK_TOKENS,\n      transform = \"none\",\n      value,\n      defaultValue,\n      onValueChange,\n      name,\n      formattedName,\n      hint,\n      placeholderChar = \"_\",\n      placeholder,\n      className,\n      disabled = false,\n      // Off by default, and overridable. A masked field is usually collecting something the browser\n      // has no entry for, and a heuristic autofill that drops an unformatted value into it is read\n      // through the mask as if it had been pasted. The mobile keyboard's own helpfulness is worse:\n      // autocapitalise and autocorrect rewrite a serial number into a word between the keypress and\n      // the change event. A field that does have an entry — a postal code — should pass its own\n      // `autoComplete=\"postal-code\"`.\n      autoCapitalize = \"off\",\n      autoComplete = \"off\",\n      autoCorrect = \"off\",\n      spellCheck = false,\n      onKeyDown,\n      ...props\n    },\n    forwardedRef\n  ) {\n    // Compiled every render rather than memoised. A mask is a handful of characters, and the cache\n    // key would have to include the identity of the regular expressions in `tokens` — which a\n    // caller writing them inline changes on every render anyway, so the memo would never hit while\n    // still being able to go stale.\n    const compiled = compileMask(mask, tokens)\n    const slots = fillCount(compiled)\n\n    const valueIsControlled = value !== undefined\n\n    const [innerValue, setInnerValue] = React.useState<string>(\n      () => readThroughMask(valueIsControlled ? value ?? \"\" : defaultValue ?? \"\", compiled, transform).raw\n    )\n\n    const raw = innerValue\n    const display = layout(raw, compiled)\n\n    const generatedId = React.useId()\n    const hintId = `${generatedId}-hint`\n    const hintText = hint === undefined ? describeMask(mask, { tokens }) : hint\n\n    const inputRef = React.useRef<HTMLInputElement>(null)\n    React.useImperativeHandle(forwardedRef, () => inputRef.current as HTMLInputElement)\n\n    /**\n     * The value this component has decided on — re-synced from state each render, and written by\n     * the effects below as soon as they decide on a new one.\n     *\n     * That second half matters because both effects run in the same pass when a parent changes the\n     * mask and the value together, which is the ordinary way a country-dependent postal code field\n     * behaves. Without it, the mask effect would read the value the *previous* render held, re-clip\n     * that, and emit it over the one the value effect had just accepted.\n     */\n    const stateRef = React.useRef(raw)\n    stateRef.current = raw\n\n    /**\n     * The last value handed to `onValueChange`, and the previous props, so the effects below can\n     * tell \"the parent changed its mind\" from \"the parent has not re-rendered yet\".\n     *\n     * That distinction is the whole of the controlled-mode problem. Between a keystroke and the\n     * parent's re-render the `value` prop is genuinely stale, and a field that re-seeds from a stale\n     * prop erases the keystroke that produced it. Comparing the prop against its own previous value\n     * separates the two cases, and it is done here rather than left to a dependency array: deps\n     * decide when React *may* skip an effect, not when it must, so a component that is only correct\n     * while its effect is skipped breaks the first time something re-runs it.\n     */\n    const lastEmittedRef = React.useRef(raw)\n    const prevValueRef = React.useRef(value)\n    const prevMaskRef = React.useRef(mask)\n\n    /** Re-seed from `value`, but only on a change the parent actually made. */\n    React.useEffect(() => {\n      if (!valueIsControlled) return\n      if (value === prevValueRef.current) return\n      prevValueRef.current = value\n      const incoming = readThroughMask(value ?? \"\", compiled, transform).raw\n      lastEmittedRef.current = incoming\n      if (incoming === stateRef.current) return\n      stateRef.current = incoming\n      setInnerValue(incoming)\n    })\n\n    /**\n     * A mask that changes takes the value with it.\n     *\n     * Forms do this — a postal code field whose shape follows the country above it — and the value\n     * left behind is measured in the old mask's slots. Re-reading it through the new one is what\n     * keeps a five-digit code from sitting invisibly in state behind a four-slot field, and the\n     * parent is told because the value it is storing has genuinely changed.\n     */\n    React.useEffect(() => {\n      if (mask === prevMaskRef.current) return\n      prevMaskRef.current = mask\n      const next = readThroughMask(stateRef.current, compiled, transform).raw\n      if (next === stateRef.current) return\n      stateRef.current = next\n      setInnerValue(next)\n      lastEmittedRef.current = next\n      onValueChange?.(next, { formatted: layout(next, compiled), complete: next.length === slots })\n    })\n\n    /** Where the caret goes once React has re-rendered with the reformatted text. */\n    const caretRef = React.useRef<number | null>(null)\n    React.useLayoutEffect(() => {\n      const at = caretRef.current\n      if (at === null) return\n      caretRef.current = null\n      inputRef.current?.setSelectionRange?.(at, at)\n    })\n\n    /**\n     * The single write path. `setInnerValue` runs whether or not the value is controlled — see the\n     * effect above for why — and the caret is queued in typed characters before the text is\n     * reformatted, since the offset it will need does not exist until then.\n     */\n    function commit(nextRaw: string, caretFill: number) {\n      const clipped = nextRaw.slice(0, slots)\n      setInnerValue(clipped)\n      const formatted = layout(clipped, compiled)\n      caretRef.current = caretAfterFill(formatted, Math.min(caretFill, clipped.length), compiled)\n      lastEmittedRef.current = clipped\n      onValueChange?.(clipped, { formatted, complete: clipped.length === slots })\n    }\n\n    function handleChange(event: React.ChangeEvent<HTMLInputElement>) {\n      const text = event.target.value\n      const caret = event.target.selectionStart ?? text.length\n      const { raw: next, rawAtUpTo } = readThroughMask(text, compiled, transform, caret)\n      commit(next, rawAtUpTo)\n    }\n\n    /**\n     * Backspace and Delete are handled here rather than left to the browser because the separators\n     * were never typed and should not have to be deleted.\n     *\n     * Without this, backspacing over the dash in `123-4567` removes it, the reformat puts it\n     * straight back, and the key appears to do nothing — the single most common complaint about\n     * masked inputs. Deleting a typed character instead means the caret lands one place earlier and\n     * the separator follows it, which is what the key looked like it was going to do.\n     */\n    function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {\n      onKeyDown?.(event)\n      if (event.defaultPrevented) return\n      if (event.key !== \"Backspace\" && event.key !== \"Delete\") return\n      const el = event.currentTarget\n      const start = el.selectionStart ?? 0\n      // A selection deletes what is selected; the browser does that correctly and `handleChange`\n      // reformats what is left.\n      if (start !== (el.selectionEnd ?? start)) return\n      const before = readThroughMask(el.value, compiled, transform, start).rawAtUpTo\n      if (event.key === \"Backspace\") {\n        if (before === 0) return\n        event.preventDefault()\n        commit(raw.slice(0, before - 1) + raw.slice(before), before - 1)\n      } else {\n        if (before >= raw.length) return\n        event.preventDefault()\n        commit(raw.slice(0, before) + raw.slice(before + 1), before)\n      }\n    }\n\n    const describedBy = [hintText ? hintId : null, props[\"aria-describedby\"]]\n      .filter(Boolean)\n      .join(\" \")\n\n    return (\n      <>\n        <input\n          {...props}\n          ref={inputRef}\n          // \"text\" rather than \"number\": a part number is a string that happens to contain digits,\n          // and a number input would offer a spinner, drop a leading zero, and refuse to hold the\n          // separators at all.\n          type=\"text\"\n          // A numeric keypad on a phone whenever every slot is a digit, and the ordinary keyboard\n          // the moment one of them is not. Worth deriving rather than asking for: a postal code\n          // field that opens the full keyboard is a small tax paid by everyone who fills the form.\n          inputMode={\n            slots > 0 && compiled.every((t) => t.kind === \"literal\" || t.key === \"#\")\n              ? \"numeric\"\n              : \"text\"\n          }\n          value={display}\n          onChange={handleChange}\n          onKeyDown={handleKeyDown}\n          disabled={disabled}\n          placeholder={placeholder ?? maskPlaceholder(compiled, placeholderChar)}\n          autoCapitalize={autoCapitalize}\n          autoComplete={autoComplete}\n          autoCorrect={autoCorrect}\n          spellCheck={spellCheck}\n          aria-describedby={describedBy || undefined}\n          className={cn(\n            \"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors\",\n            \"placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring\",\n            \"disabled:cursor-not-allowed disabled:opacity-50\",\n            className\n          )}\n        />\n\n        {hintText ? (\n          <span id={hintId} className=\"sr-only\">\n            Format: {hintText}\n          </span>\n        ) : null}\n\n        {/*\n          `disabled` and `form` are mirrored onto these, because what submits is the hidden pair and\n          not the field above them. A disabled control is omitted from the form data by the browser;\n          a hidden input that was never told it is disabled is not, so a greyed-out field whose\n          value is deliberately not being collected submits it anyway — and the handler receives a\n          postal code the person could not have edited. `form` fails the other way round: the field\n          can be associated with a form elsewhere in the document by id, and hidden inputs left\n          without it stay with the ancestor they happen to sit in, so the value is submitted by the\n          wrong form or by none.\n        */}\n        {name ? (\n          <input type=\"hidden\" name={name} value={raw} disabled={disabled} form={props.form} />\n        ) : null}\n        {formattedName ? (\n          <input\n            type=\"hidden\"\n            name={formattedName}\n            value={display}\n            disabled={disabled}\n            form={props.form}\n          />\n        ) : null}\n      </>\n    )\n  }\n)\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui",
  "docs": "Install any pulld component by name: add \"@pulld\": \"https://pulld.pages.dev/r/{name}.json\" to the registries block in components.json, then `npx shadcn add @pulld/<name>`. All 140 components: https://pulld.pages.dev/?utm_source=cli"
}
