{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "price-input",
  "title": "Price Input",
  "description": "A text input that live-formats digits as a grouped price as you type, with support for Persian/Arabic-Indic numerals.",
  "registryDependencies": [
    "utils",
    "@persianlabsui/input",
    "@persianlabsui/normalize-persian-digits"
  ],
  "files": [
    {
      "path": "registry/base/ui/price-input.tsx",
      "content": "\"use client\"\n\nimport type * as React from \"react\"\n\nimport { Input } from \"@/components/ui/input\"\nimport { useControllableState } from \"@/hooks/use-controllable-state\"\nimport { normalizePersianDigits } from \"@/lib/normalize-persian-digits\"\nimport { cn } from \"@/lib/utils\"\n\nexport interface PriceInputProps extends Omit<\n  React.ComponentProps<typeof Input>,\n  \"value\" | \"defaultValue\" | \"onChange\" | \"type\"\n> {\n  /** The numeric value. Use when controlled. */\n  value?: number | null\n  /** The initial numeric value when uncontrolled. */\n  defaultValue?: number | null\n  /** Called with the numeric value whenever it changes. */\n  onValueChange?: (value: number | null) => void\n  /** The minimum allowed value, applied on blur. */\n  min?: number\n  /** The maximum allowed value, applied on blur. */\n  max?: number\n  /** The locale used to group digits as you type. @default \"en-US\" */\n  locale?: Intl.LocalesArgument\n}\n\nfunction clamp(value: number, min?: number, max?: number) {\n  let next = value\n  if (min !== undefined) next = Math.max(next, min)\n  if (max !== undefined) next = Math.min(next, max)\n  return next\n}\n\nfunction formatValue(value: number | null, locale: Intl.LocalesArgument) {\n  if (value === null) return \"\"\n  return new Intl.NumberFormat(locale, { maximumFractionDigits: 0 }).format(\n    value\n  )\n}\n\nfunction countDigits(text: string) {\n  return (text.match(/\\d/g) ?? []).length\n}\n\n/** The index right after the Nth digit in `text` (commas/signs don't count). */\nfunction offsetAfterDigitCount(text: string, digitCount: number) {\n  if (digitCount <= 0) return 0\n\n  let seen = 0\n  for (let i = 0; i < text.length; i++) {\n    if (/\\d/.test(text.charAt(i))) {\n      seen++\n      if (seen === digitCount) return i + 1\n    }\n  }\n  return text.length\n}\n\n/**\n * Deleting a thousands separator removes zero digits, so reformatting\n * would snap the text right back and the keystroke would visibly do\n * nothing. When that happens, extend the deletion to the nearest digit\n * in the same direction, so Delete/Backspace always removes one digit\n * no matter which character the caret sits next to.\n */\nfunction extendDeletionToDigit(\n  raw: string,\n  cursor: number,\n  direction: \"forward\" | \"backward\"\n) {\n  if (direction === \"forward\") {\n    const after = raw.slice(cursor)\n    const relativeIndex = after.search(/\\d/)\n    if (relativeIndex === -1) return raw\n    const index = cursor + relativeIndex\n    return raw.slice(0, index) + raw.slice(index + 1)\n  }\n\n  const before = raw.slice(0, cursor)\n  const digitMatches = [...before.matchAll(/\\d/g)]\n  const lastDigit = digitMatches.at(-1)\n  if (!lastDigit || lastDigit.index === undefined) return raw\n  return raw.slice(0, lastDigit.index) + raw.slice(lastDigit.index + 1)\n}\n\nfunction PriceInput({\n  value,\n  defaultValue = null,\n  onValueChange,\n  min,\n  max,\n  locale = \"en-US\",\n  placeholder = \"0\",\n  className,\n  onBlur,\n  ...props\n}: PriceInputProps) {\n  const [numericValue, setNumericValue] = useControllableState({\n    prop: value,\n    defaultProp: defaultValue,\n    onChange: onValueChange,\n    caller: \"PriceInput\",\n  })\n\n  function handleChange(event: React.ChangeEvent<HTMLInputElement>) {\n    const input = event.target\n    const cursor = input.selectionStart ?? input.value.length\n    let raw = normalizePersianDigits(input.value)\n\n    const inputType = (event.nativeEvent as InputEvent).inputType\n    const previousDigitCount =\n      numericValue === null ? 0 : String(Math.abs(numericValue)).length\n    let digits = raw.replace(/\\D/g, \"\")\n\n    if (\n      (inputType === \"deleteContentForward\" ||\n        inputType === \"deleteContentBackward\") &&\n      digits.length === previousDigitCount\n    ) {\n      raw = extendDeletionToDigit(\n        raw,\n        cursor,\n        inputType === \"deleteContentForward\" ? \"forward\" : \"backward\"\n      )\n      digits = raw.replace(/\\D/g, \"\")\n    }\n\n    const digitsBeforeCursor = countDigits(raw.slice(0, cursor))\n    const negative =\n      min === undefined || min < 0 ? raw.trimStart().startsWith(\"-\") : false\n\n    if (!digits) {\n      setNumericValue(null)\n      return\n    }\n\n    const nextValue = (negative ? -1 : 1) * Number(digits)\n    const formatted = formatValue(nextValue, locale)\n\n    // Reformatting shifts thousands separators around, which would\n    // otherwise reset the caret to the end of the input on every\n    // keystroke — restore it based on digit count instead of raw offset.\n    if (formatted !== input.value) {\n      const nextPos = offsetAfterDigitCount(formatted, digitsBeforeCursor)\n      input.value = formatted\n      input.setSelectionRange(nextPos, nextPos)\n    }\n\n    setNumericValue(nextValue)\n  }\n\n  function handleBlur(event: React.FocusEvent<HTMLInputElement>) {\n    if (numericValue !== null) {\n      const clamped = clamp(numericValue, min, max)\n      if (clamped !== numericValue) setNumericValue(clamped)\n    }\n    onBlur?.(event)\n  }\n\n  return (\n    <Input\n      {...props}\n      type=\"text\"\n      inputMode={min !== undefined && min >= 0 ? \"numeric\" : \"text\"}\n      dir=\"ltr\"\n      value={formatValue(numericValue, locale)}\n      onChange={handleChange}\n      onBlur={handleBlur}\n      placeholder={placeholder}\n      className={cn(\"font-mono\", className)}\n    />\n  )\n}\n\nexport { PriceInput }\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"
}