{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "bank-input",
  "title": "Bank Input",
  "description": "Iranian card-number and Shaba inputs with paste normalization, checksum validation, and bank detection.",
  "dependencies": [
    "@persianlabs/icons"
  ],
  "registryDependencies": [
    "utils",
    "@persianlabsui/input-group",
    "@persianlabsui/iranian-bank"
  ],
  "files": [
    {
      "path": "registry/base/ui/bank-input.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as BankIcons from \"@persianlabs/icons/react\"\n\nimport {\n  InputGroup,\n  InputGroupAddon,\n  InputGroupInput,\n  InputGroupText,\n} from \"@/components/ui/input-group\"\nimport { useControllableState } from \"@/hooks/use-controllable-state\"\nimport {\n  formatCardNumber,\n  formatShaba,\n  getIranianBankByCardNumber,\n  getIranianBankByShaba,\n  normalizeCardNumber,\n  normalizeShaba,\n  type IranianBank,\n  type IranianBankId,\n} from \"@/lib/iranian-bank\"\nimport { cn } from \"@/lib/utils\"\n\nexport type BankLogoVariant = \"color\" | \"mono\" | false\n\ntype BaseBankInputProps = Omit<\n  React.ComponentProps<typeof InputGroupInput>,\n  \"value\" | \"defaultValue\" | \"onChange\" | \"type\" | \"dir\"\n> & {\n  /** Renders a color or monochrome logo once a bank is detected. @default \"color\" */\n  bankLogo?: BankLogoVariant\n  /** Receives the detected bank as soon as enough digits are entered. */\n  onBankChange?: (bank: IranianBank | undefined) => void\n  /** The separator inserted after each four-digit group. */\n  separator?: string\n}\n\nexport interface CardNumberInputProps extends BaseBankInputProps {\n  value?: string\n  defaultValue?: string\n  onValueChange?: (value: string) => void\n}\n\nexport interface ShabaInputProps extends BaseBankInputProps {\n  /** The account portion only, without the visible IR prefix. */\n  value?: string\n  defaultValue?: string\n  onValueChange?: (value: string) => void\n}\n\nconst iconNames: Record<IranianBankId, string> = {\n  blubank: \"BankBlubank\",\n  dey: \"BankDey\",\n  eghtesad_novin: \"BankEghtesadNovin\",\n  gardeshgari: \"BankGardeshgari\",\n  ghavvamin: \"BankGhavamin\",\n  hekmat: \"BankHekmat\",\n  iranzamin: \"BankIranZamin\",\n  kar_afarin: \"BankKarafarin\",\n  keshavarzi: \"BankKeshavarzi\",\n  khavarmianeh: \"BankKhavarMianeh\",\n  maskan: \"BankMaskan\",\n  mehr_e_iranian: \"BankMehrIran\",\n  meli: \"BankMelall\",\n  mellat: \"BankMellat\",\n  melli: \"BankMelli\",\n  parsian: \"BankParsian\",\n  pasargad: \"BankPasargad\",\n  post_bank: \"BankPostbank\",\n  refah: \"BankRefah\",\n  resalat: \"BankResalat\",\n  saderat: \"BankSaderat\",\n  saman: \"BankSaman\",\n  sarmayeh: \"BankSarmayeh\",\n  sepah: \"BankSepah\",\n  shahr: \"BankShahr\",\n  sina: \"BankSina\",\n  tejarat: \"BankTejarat\",\n  tosee_saderat: \"BankToseeSaderat\",\n  tosee_taavon: \"BankToseeTaavon\",\n}\n\nfunction offsetAfterDigits(value: string, digitCount: number) {\n  if (digitCount <= 0) return 0\n  let seen = 0\n  for (let index = 0; index < value.length; index++) {\n    if (/\\d/.test(value[index]!)) seen++\n    if (seen === digitCount) return index + 1\n  }\n  return value.length\n}\n\nfunction restoreCaret(\n  input: HTMLInputElement,\n  formatted: string,\n  digitsBeforeCaret: number\n) {\n  requestAnimationFrame(() => {\n    const offset = offsetAfterDigits(formatted, digitsBeforeCaret)\n    input.setSelectionRange(offset, offset)\n  })\n}\n\nfunction BankIdentity({\n  bank,\n  logo,\n}: {\n  bank: IranianBank | undefined\n  logo: BankLogoVariant\n}) {\n  if (!bank) return null\n  const Logo = BankIcons[\n    `${iconNames[bank.id]}${logo === \"mono\" ? \"Mono\" : \"Color\"}` as keyof typeof BankIcons\n  ] as React.ComponentType<React.SVGProps<SVGSVGElement>>\n\n  return (\n    <InputGroupAddon align=\"inline-end\" className=\"pe-2\">\n      <Logo className=\"size-5 shrink-0\" aria-hidden=\"true\" />\n    </InputGroupAddon>\n  )\n}\n\nfunction CardNumberInput({\n  value,\n  defaultValue = \"\",\n  onValueChange,\n  bankLogo = \"color\",\n  onBankChange,\n  separator = \" \",\n  className,\n  ...props\n}: CardNumberInputProps) {\n  const [number, setNumber] = useControllableState({\n    prop: value,\n    defaultProp: defaultValue,\n    onChange: onValueChange,\n    caller: \"CardNumberInput\",\n  })\n  const normalized = normalizeCardNumber(number)\n  const bank = getIranianBankByCardNumber(normalized)\n\n  function handleChange(event: React.ChangeEvent<HTMLInputElement>) {\n    const digitsBeforeCaret = normalizeCardNumber(\n      event.target.value.slice(0, event.target.selectionStart ?? 0)\n    ).length\n    const next = normalizeCardNumber(event.target.value)\n    setNumber(next)\n    onBankChange?.(getIranianBankByCardNumber(next))\n    restoreCaret(\n      event.target,\n      formatCardNumber(next, separator),\n      digitsBeforeCaret\n    )\n  }\n\n  return (\n    <InputGroup className={cn(\"h-10\", className)} dir=\"ltr\">\n      <InputGroupInput\n        {...props}\n        dir=\"ltr\"\n        inputMode=\"numeric\"\n        autoComplete=\"cc-number\"\n        spellCheck={false}\n        translate=\"no\"\n        value={formatCardNumber(normalized, separator)}\n        onChange={handleChange}\n        placeholder={`1234${separator}1234${separator}1234${separator}1234`}\n        className=\"font-mono text-sm tracking-wide md:text-base\"\n      />\n      {bankLogo && <BankIdentity bank={bank} logo={bankLogo} />}\n    </InputGroup>\n  )\n}\n\nfunction ShabaInput({\n  value,\n  defaultValue = \"\",\n  onValueChange,\n  bankLogo = \"color\",\n  onBankChange,\n  separator = \" \",\n  className,\n  ...props\n}: ShabaInputProps) {\n  const [account, setAccount] = useControllableState({\n    prop: value,\n    defaultProp: defaultValue,\n    onChange: onValueChange,\n    caller: \"ShabaInput\",\n  })\n  const normalized = normalizeShaba(account)\n  const bank = getIranianBankByShaba(normalized)\n\n  function handleChange(event: React.ChangeEvent<HTMLInputElement>) {\n    const digitsBeforeCaret = normalizeShaba(\n      event.target.value.slice(0, event.target.selectionStart ?? 0)\n    ).length\n    const next = normalizeShaba(event.target.value)\n    setAccount(next)\n    onBankChange?.(getIranianBankByShaba(next))\n    restoreCaret(event.target, formatShaba(next, separator), digitsBeforeCaret)\n  }\n\n  return (\n    <InputGroup className={cn(\"h-10\", className)} dir=\"ltr\">\n      <InputGroupAddon align=\"inline-start\">\n        <InputGroupText className=\"font-mono font-semibold tracking-wide\">\n          IR\n        </InputGroupText>\n      </InputGroupAddon>\n      <InputGroupInput\n        {...props}\n        dir=\"ltr\"\n        inputMode=\"numeric\"\n        autoComplete=\"off\"\n        spellCheck={false}\n        translate=\"no\"\n        value={formatShaba(normalized, separator)}\n        onChange={handleChange}\n        placeholder={`1234${separator}1234${separator}1234${separator}1234${separator}1234${separator}1234`}\n        className=\"font-mono text-sm tracking-wide md:text-base\"\n      />\n      {bankLogo && <BankIdentity bank={bank} logo={bankLogo} />}\n    </InputGroup>\n  )\n}\n\nexport { CardNumberInput, ShabaInput }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/base/hooks/use-controllable-state.ts",
      "content": "// Thanks @radix-ui\n\nimport * as React from \"react\"\n\n// use-layout-effect.tsx\n// https://github.com/radix-ui/primitives/blob/main/packages/react/use-layout-effect/src/use-layout-effect.tsx\n\n/**\n * On the server, React emits a warning when calling `useLayoutEffect`.\n * This is because neither `useLayoutEffect` nor `useEffect` run on the server.\n * We use this safe version which suppresses the warning by replacing it with a noop on the server.\n *\n * See: https://reactjs.org/docs/hooks-reference.html#uselayouteffect\n */\nconst useLayoutEffect = globalThis?.document ? React.useLayoutEffect : () => {}\n\n// use-controllable-state.tsx\n// https://github.com/radix-ui/primitives/blob/main/packages/react/use-controllable-state/src/use-controllable-state.tsx\n\n// Prevent bundlers from trying to optimize the import\nconst useInsertionEffect: typeof useLayoutEffect =\n  (React as never)[\" useInsertionEffect \".trim().toString()] || useLayoutEffect\n\ntype ChangeHandler<T> = (state: T) => void\ntype SetStateFn<T> = React.Dispatch<React.SetStateAction<T>>\n\ninterface UseControllableStateParams<T> {\n  prop?: T | undefined\n  defaultProp: T\n  onChange?: ChangeHandler<T>\n  caller?: string\n}\n\nexport function useControllableState<T>({\n  prop,\n  defaultProp,\n  onChange = () => {},\n  caller,\n}: UseControllableStateParams<T>): [T, SetStateFn<T>] {\n  const [uncontrolledProp, setUncontrolledProp, onChangeRef] =\n    useUncontrolledState({\n      defaultProp,\n      onChange,\n    })\n  const isControlled = prop !== undefined\n  const value = isControlled ? prop : uncontrolledProp\n\n  // OK to disable conditionally calling hooks here because they will always run\n  // consistently in the same environment. Bundlers should be able to remove the\n  // code block entirely in production.\n  /* eslint-disable react-hooks/rules-of-hooks */\n  if (process.env.NODE_ENV !== \"production\") {\n    const isControlledRef = React.useRef(prop !== undefined)\n    React.useEffect(() => {\n      const wasControlled = isControlledRef.current\n      if (wasControlled !== isControlled) {\n        const from = wasControlled ? \"controlled\" : \"uncontrolled\"\n        const to = isControlled ? \"controlled\" : \"uncontrolled\"\n        console.warn(\n          `${caller} is changing from ${from} to ${to}. Components should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled value for the lifetime of the component.`\n        )\n      }\n      isControlledRef.current = isControlled\n    }, [isControlled, caller])\n  }\n  /* eslint-enable react-hooks/rules-of-hooks */\n\n  const setValue = React.useCallback<SetStateFn<T>>(\n    (nextValue) => {\n      if (isControlled) {\n        const value = isFunction(nextValue) ? nextValue(prop) : nextValue\n        if (value !== prop) {\n          onChangeRef.current?.(value)\n        }\n      } else {\n        setUncontrolledProp(nextValue)\n      }\n    },\n    [isControlled, prop, setUncontrolledProp, onChangeRef]\n  )\n\n  return [value, setValue]\n}\n\nfunction useUncontrolledState<T>({\n  defaultProp,\n  onChange,\n}: Omit<UseControllableStateParams<T>, \"prop\">): [\n  Value: T,\n  setValue: React.Dispatch<React.SetStateAction<T>>,\n  OnChangeRef: React.RefObject<ChangeHandler<T> | undefined>,\n] {\n  const [value, setValue] = React.useState(defaultProp)\n  const prevValueRef = React.useRef(value)\n\n  const onChangeRef = React.useRef(onChange)\n  useInsertionEffect(() => {\n    onChangeRef.current = onChange\n  }, [onChange])\n\n  React.useEffect(() => {\n    if (prevValueRef.current !== value) {\n      onChangeRef.current?.(value)\n      prevValueRef.current = value\n    }\n  }, [value, prevValueRef])\n\n  return [value, setValue, onChangeRef]\n}\n\nfunction isFunction(value: unknown): value is (...args: never[]) => unknown {\n  return typeof value === \"function\"\n}\n",
      "type": "registry:hook"
    }
  ],
  "type": "registry:ui"
}