{"name":"rhf-currency-field","title":"RHF Currency Field","description":"Currency input with symbol and React Hook Form integration.","type":"registry:ui","docs":"/components/rhf-currency-field","categories":["forms"],"registryDependencies":["https://pb-ui-five.vercel.app/registry/rhf-base-controller","https://pb-ui-five.vercel.app/registry/input"],"dependencies":["react-hook-form"],"files":[{"path":"components/ui/rhf-inputs/currency-field.tsx","target":"components/ui/rhf-inputs/currency-field.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport * as React from \"react\";\nimport { FieldValues } from \"react-hook-form\";\nimport { Input } from \"../input\";\nimport { BaseController, BaseControllerProps } from \"./base-controller\";\n\ntype CurrencyFieldProps<T extends FieldValues> = Omit<\n  BaseControllerProps<T>,\n  \"children\"\n> & {\n  currency?: string;\n  locale?: string;\n  min?: number;\n  max?: number;\n  placeholder?: string;\n  disabled?: boolean;\n  className?: string;\n  allowCents?: boolean;\n};\n\nfunction CurrencyInput({\n  value,\n  onChange,\n  onBlur,\n  min,\n  max,\n  currencySymbol,\n  decimalSeparator,\n  placeholder,\n  disabled,\n  id,\n  error,\n  required,\n  allowCents = false,\n  formatValue,\n  ariaDescribedBy,\n}: {\n  value: number | null | undefined;\n  onChange: (value: number | null) => void;\n  onBlur: () => void;\n  min?: number;\n  max?: number;\n  currencySymbol: string;\n  decimalSeparator: string;\n  placeholder?: string;\n  disabled?: boolean;\n  id: string;\n  error?: boolean;\n  required?: boolean;\n  allowCents?: boolean;\n  formatValue: (value: number) => string;\n  ariaDescribedBy?: string;\n}) {\n  const [inputValue, setInputValue] = React.useState<string>(\n    value != null ? formatValue(value) : \"\",\n  );\n  const [isFocused, setIsFocused] = React.useState(false);\n\n  React.useEffect(() => {\n    if (!isFocused) {\n      setInputValue(value != null ? formatValue(value) : \"\");\n    }\n  }, [value, formatValue, isFocused]);\n\n  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    const input = e.target.value;\n\n    // Allow digits, minus, and the locale-specific decimal separator\n    const escapedSep = decimalSeparator.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n    const pattern = allowCents\n      ? new RegExp(`[^0-9${escapedSep}-]`, \"g\")\n      : /[^0-9-]/g;\n\n    const raw = input.replace(pattern, \"\");\n\n    // Normalize decimal separator to period for parsing\n    const normalized = raw.replace(decimalSeparator, \".\");\n\n    setInputValue(raw);\n\n    if (normalized === \"\" || normalized === \"-\") {\n      onChange(null);\n      return;\n    }\n\n    const parsed = allowCents\n      ? parseFloat(normalized)\n      : parseInt(normalized, 10);\n    if (!isNaN(parsed)) {\n      let clamped = parsed;\n      if (min != null) clamped = Math.max(min, clamped);\n      if (max != null) clamped = Math.min(max, clamped);\n      onChange(clamped);\n    }\n  };\n\n  const handleFocus = () => {\n    setIsFocused(true);\n    // Show raw number when focused for easier editing\n    if (value != null) {\n      const rawValue = allowCents\n        ? String(value).replace(\".\", decimalSeparator)\n        : String(value);\n      setInputValue(rawValue);\n    }\n  };\n\n  const handleBlurInput = () => {\n    setIsFocused(false);\n    // Format value on blur\n    if (value != null) {\n      setInputValue(formatValue(value));\n    } else {\n      setInputValue(\"\");\n    }\n    onBlur();\n  };\n\n  return (\n    <div className=\"relative\">\n      <span className=\"top-1/2 left-3 absolute text-muted-foreground text-sm -translate-y-1/2 pointer-events-none\">\n        {currencySymbol}\n      </span>\n      <Input\n        id={id}\n        type=\"text\"\n        inputMode=\"decimal\"\n        value={inputValue}\n        onChange={handleChange}\n        onFocus={handleFocus}\n        onBlur={handleBlurInput}\n        placeholder={placeholder}\n        disabled={disabled}\n        aria-invalid={error}\n        aria-required={required}\n        aria-describedby={ariaDescribedBy}\n        className=\"pl-7\"\n      />\n    </div>\n  );\n}\n\nexport function CurrencyField<T extends FieldValues>({\n  control,\n  name,\n  label,\n  description,\n  required,\n  disableFieldError = false,\n  currency = \"USD\",\n  locale = \"en-US\",\n  min,\n  max,\n  placeholder,\n  disabled = false,\n  className,\n  allowCents = false,\n}: CurrencyFieldProps<T>) {\n  const currencySymbol = React.useMemo(() => {\n    return (\n      new Intl.NumberFormat(locale, {\n        style: \"currency\",\n        currency,\n        minimumFractionDigits: 0,\n        maximumFractionDigits: 0,\n      })\n        .formatToParts(0)\n        .find((part) => part.type === \"currency\")?.value ?? \"$\"\n    );\n  }, [locale, currency]);\n\n  const decimalSeparator = React.useMemo(() => {\n    return (\n      new Intl.NumberFormat(locale)\n        .formatToParts(1.1)\n        .find((part) => part.type === \"decimal\")?.value ?? \".\"\n    );\n  }, [locale]);\n\n  const formatValue = React.useCallback(\n    (value: number) => {\n      return new Intl.NumberFormat(locale, {\n        minimumFractionDigits: allowCents ? 2 : 0,\n        maximumFractionDigits: allowCents ? 2 : 0,\n      }).format(value);\n    },\n    [locale, allowCents],\n  );\n\n  return (\n    <BaseController\n      control={control}\n      name={name}\n      label={label}\n      required={required}\n      description={description}\n      disableFieldError={disableFieldError}\n    >\n      {({ field, fieldState, ariaDescribedBy }) => (\n        <div className={cn(className)}>\n          <CurrencyInput\n            value={field.value}\n            onChange={(value) => field.onChange(value)}\n            onBlur={field.onBlur}\n            min={min}\n            max={max}\n            currencySymbol={currencySymbol}\n            decimalSeparator={decimalSeparator}\n            placeholder={placeholder}\n            disabled={disabled}\n            id={field.name}\n            error={!!fieldState.error}\n            required={required}\n            allowCents={allowCents}\n            formatValue={formatValue}\n            ariaDescribedBy={ariaDescribedBy}\n          />\n        </div>\n      )}\n    </BaseController>\n  );\n}\n"}]}