{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "form-fields",
  "title": "Form Fields",
  "author": "Poyraz Avsever",
  "description": "Poyraz Soft Glass form-fields component.",
  "dependencies": [
    "lucide-react@^0.574.0"
  ],
  "registryDependencies": [
    "@poyraz/poyraz-utils",
    "@poyraz/poyraz-theme",
    "@poyraz/input",
    "@poyraz/button"
  ],
  "files": [
    {
      "path": "registry/poyraz/ui/phase5/atoms/form-fields.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Minus, Plus, Search, Eye, EyeOff, Phone, Globe } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Input, InputGroup, InputGroupAddon, type InputProps } from \"@/components/ui/atoms/input\";\nimport { Button } from \"@/components/ui/atoms/button\";\n\n/* ================================================================== */\n/*  SHARED WRAPPER                                                     */\n/* ================================================================== */\n\nconst innerInput = [\n  \"min-w-0 border-0 bg-transparent shadow-none outline-none ring-0 ring-offset-0 focus-visible:border-0 focus-visible:ring-0 focus-visible:ring-offset-0\",\n].join(\" \");\n\ntype MaskToken = \"#\" | \"A\" | \"*\";\nconst maskRules: Record<MaskToken, RegExp> = {\n  \"#\": /\\d/,\n  A: /[a-zA-Z]/,\n  \"*\": /[a-zA-Z0-9]/,\n};\n\nfunction applyInputMask(value: string, mask: string) {\n  const source = value.replace(/[^a-zA-Z0-9]/g, \"\");\n  let sourceIndex = 0;\n  let result = \"\";\n\n  for (const character of mask) {\n    const rule = maskRules[character as MaskToken];\n    if (!rule) {\n      if (source.length > 0 && (sourceIndex > 0 || result.length === 0)) result += character;\n      continue;\n    }\n\n    while (sourceIndex < source.length && !rule.test(source[sourceIndex])) sourceIndex += 1;\n    if (sourceIndex >= source.length) break;\n    result += source[sourceIndex];\n    sourceIndex += 1;\n  }\n\n  return result;\n}\n\nexport interface MaskedInputProps extends Omit<InputProps, \"value\" | \"defaultValue\"> {\n  mask: string;\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (formattedValue: string, rawValue: string) => void;\n}\n\nconst MaskedInput = React.forwardRef<HTMLInputElement, MaskedInputProps>(\n  ({ defaultValue, mask, onChange, onValueChange, value, ...props }, ref) => {\n    const format = (input: string) => applyInputMask(input, mask);\n    const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {\n      const formatted = format(event.currentTarget.value);\n      event.currentTarget.value = formatted;\n      onValueChange?.(formatted, formatted.replace(/[^a-zA-Z0-9]/g, \"\"));\n      onChange?.(event);\n    };\n\n    return (\n      <Input\n        ref={ref}\n        value={value === undefined ? undefined : format(value)}\n        defaultValue={defaultValue === undefined ? undefined : format(defaultValue)}\n        maxLength={mask.length}\n        onChange={handleChange}\n        {...props}\n      />\n    );\n  },\n);\nMaskedInput.displayName = \"MaskedInput\";\n\n/* ================================================================== */\n/*  NUMBER INPUT                                                       */\n/* ================================================================== */\n\nexport interface NumberInputProps extends Omit<InputProps, \"type\" | \"onChange\" | \"value\"> {\n  value?: number;\n  onChange?: (value: number) => void;\n  min?: number;\n  max?: number;\n  step?: number;\n}\n\nconst NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(\n  (\n    { className, value = 0, onChange, min, max, step = 1, disabled, radius, variant, ...props },\n    ref,\n  ) => {\n    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n      const num = parseFloat(e.target.value);\n      if (!isNaN(num)) onChange?.(clamp(num, min, max));\n    };\n\n    const increment = () => {\n      onChange?.(clamp((value ?? 0) + step, min, max));\n    };\n\n    const decrement = () => {\n      onChange?.(clamp((value ?? 0) - step, min, max));\n    };\n\n    return (\n      <InputGroup\n        data-slot=\"number-input\"\n        radius={radius}\n        variant={variant}\n        className={cn(disabled && \"opacity-60\", className)}\n      >\n        <Button\n          type=\"button\"\n          variant=\"ghost\"\n          size=\"icon\"\n          radius=\"none\"\n          className=\"h-full w-10 shrink-0 border-0\"\n          onClick={decrement}\n          disabled={disabled || (min !== undefined && (value ?? 0) <= min)}\n          tabIndex={-1}\n          aria-label=\"Decrease\"\n        >\n          <Minus className=\"h-4 w-4\" />\n        </Button>\n        <Input\n          ref={ref}\n          type=\"number\"\n          value={value}\n          onChange={handleChange}\n          disabled={disabled}\n          aria-invalid={props[\"aria-invalid\"]}\n          className={cn(\n            innerInput,\n            \"text-center [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none [-moz-appearance:textfield]\",\n          )}\n          min={min}\n          max={max}\n          step={step}\n          {...props}\n        />\n        <Button\n          type=\"button\"\n          variant=\"ghost\"\n          size=\"icon\"\n          radius=\"none\"\n          className=\"h-full w-10 shrink-0 border-0\"\n          onClick={increment}\n          disabled={disabled || (max !== undefined && (value ?? 0) >= max)}\n          tabIndex={-1}\n          aria-label=\"Increase\"\n        >\n          <Plus className=\"h-4 w-4\" />\n        </Button>\n      </InputGroup>\n    );\n  },\n);\nNumberInput.displayName = \"NumberInput\";\n\nfunction clamp(val: number, min?: number, max?: number) {\n  if (min !== undefined && val < min) return min;\n  if (max !== undefined && val > max) return max;\n  return val;\n}\n\n/* ================================================================== */\n/*  SEARCH INPUT                                                       */\n/* ================================================================== */\n\nexport interface SearchInputProps extends InputProps {\n  onSearch?: (value: string) => void;\n}\n\nconst SearchInput = React.forwardRef<HTMLInputElement, SearchInputProps>(\n  ({ className, onSearch, onKeyDown, radius, variant, ...props }, ref) => {\n    const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n      if (e.key === \"Enter\") {\n        onSearch?.((e.target as HTMLInputElement).value);\n      }\n      onKeyDown?.(e);\n    };\n\n    return (\n      <InputGroup data-slot=\"search-input\" radius={radius} variant={variant} className={className}>\n        <InputGroupAddon position=\"start\" className=\"border-0 pr-0\">\n          <Search className=\"h-4 w-4\" />\n        </InputGroupAddon>\n        <Input\n          ref={ref}\n          type=\"search\"\n          onKeyDown={handleKeyDown}\n          className={cn(innerInput)}\n          {...props}\n        />\n      </InputGroup>\n    );\n  },\n);\nSearchInput.displayName = \"SearchInput\";\n\n/* ================================================================== */\n/*  PHONE INPUT                                                        */\n/* ================================================================== */\n\nexport interface PhoneInputProps extends Omit<InputProps, \"value\" | \"defaultValue\"> {\n  /** Country code prefix, e.g. \"+90\" */\n  countryCode?: string;\n  /** `#` digit, `A` letter and `*` alphanumeric mask tokens. */\n  mask?: string | false;\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (formattedValue: string, rawDigits: string) => void;\n}\n\nconst PhoneInput = React.forwardRef<HTMLInputElement, PhoneInputProps>(\n  (\n    {\n      className,\n      countryCode = \"+1\",\n      defaultValue,\n      mask = \"(###) ### ## ##\",\n      onValueChange,\n      placeholder,\n      value,\n      radius,\n      variant,\n      ...props\n    },\n    ref,\n  ) => {\n    return (\n      <InputGroup data-slot=\"phone-input\" radius={radius} variant={variant} className={className}>\n        <InputGroupAddon position=\"start\" className=\"border-0 pr-0\">\n          <Phone className=\"h-4 w-4\" />\n        </InputGroupAddon>\n        <span\n          data-slot=\"phone-prefix\"\n          className=\"select-none whitespace-nowrap border-r border-border px-2 text-sm font-medium text-muted-foreground\"\n        >\n          {countryCode}\n        </span>\n        {mask ? (\n          <MaskedInput\n            ref={ref}\n            type=\"tel\"\n            inputMode=\"tel\"\n            mask={mask}\n            value={value}\n            defaultValue={defaultValue}\n            onValueChange={onValueChange}\n            placeholder={placeholder ?? mask.replace(/#/g, \"0\")}\n            className={cn(innerInput)}\n            {...props}\n          />\n        ) : (\n          <Input\n            ref={ref}\n            type=\"tel\"\n            inputMode=\"tel\"\n            value={value}\n            defaultValue={defaultValue}\n            placeholder={placeholder}\n            className={cn(innerInput)}\n            {...props}\n          />\n        )}\n      </InputGroup>\n    );\n  },\n);\nPhoneInput.displayName = \"PhoneInput\";\n\n/* ================================================================== */\n/*  PASSWORD INPUT                                                     */\n/* ================================================================== */\n\nconst PasswordInput = React.forwardRef<HTMLInputElement, InputProps>(\n  ({ className, radius, variant, ...props }, ref) => {\n    const [visible, setVisible] = React.useState(false);\n\n    return (\n      <InputGroup\n        data-slot=\"password-input\"\n        radius={radius}\n        variant={variant}\n        className={className}\n      >\n        <Input\n          ref={ref}\n          type={visible ? \"text\" : \"password\"}\n          className={cn(innerInput)}\n          {...props}\n        />\n        <Button\n          type=\"button\"\n          variant=\"ghost\"\n          size=\"icon\"\n          radius=\"none\"\n          className=\"h-full w-10 shrink-0 border-0\"\n          onClick={() => setVisible((v) => !v)}\n          tabIndex={-1}\n          aria-label={visible ? \"Hide password\" : \"Show password\"}\n        >\n          {visible ? <EyeOff className=\"h-4 w-4\" /> : <Eye className=\"h-4 w-4\" />}\n        </Button>\n      </InputGroup>\n    );\n  },\n);\nPasswordInput.displayName = \"PasswordInput\";\n\n/* ================================================================== */\n/*  URL INPUT                                                          */\n/* ================================================================== */\n\nexport interface UrlInputProps extends Omit<InputProps, \"value\" | \"defaultValue\"> {\n  protocol?: \"https://\" | \"http://\" | \"\";\n  normalize?: boolean;\n  value?: string;\n  defaultValue?: string;\n  onValueChange?: (value: string, absoluteUrl: string) => void;\n}\n\nconst UrlInput = React.forwardRef<HTMLInputElement, UrlInputProps>(\n  (\n    {\n      className,\n      defaultValue,\n      normalize = true,\n      onChange,\n      onValueChange,\n      protocol = \"https://\",\n      value,\n      radius,\n      variant,\n      ...props\n    },\n    ref,\n  ) => {\n    const normalizeUrl = (input: string) =>\n      normalize\n        ? input\n            .trim()\n            .replace(/^https?:\\/\\//i, \"\")\n            .replace(/^\\/\\//, \"\")\n            .replace(/\\s+/g, \"\")\n        : input;\n    const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {\n      const normalized = normalizeUrl(event.currentTarget.value);\n      event.currentTarget.value = normalized;\n      onValueChange?.(normalized, `${protocol}${normalized}`);\n      onChange?.(event);\n    };\n    return (\n      <InputGroup data-slot=\"url-input\" radius={radius} variant={variant} className={className}>\n        <InputGroupAddon position=\"start\" className=\"border-0 pr-0\">\n          <Globe className=\"h-4 w-4\" />\n        </InputGroupAddon>\n        {protocol && (\n          <span\n            data-slot=\"url-prefix\"\n            className=\"select-none whitespace-nowrap border-r border-border px-2 text-sm text-muted-foreground\"\n          >\n            {protocol}\n          </span>\n        )}\n        <Input\n          ref={ref}\n          type=\"text\"\n          inputMode=\"url\"\n          value={value === undefined ? undefined : normalizeUrl(value)}\n          defaultValue={defaultValue === undefined ? undefined : normalizeUrl(defaultValue)}\n          onChange={handleChange}\n          className={cn(innerInput)}\n          {...props}\n        />\n      </InputGroup>\n    );\n  },\n);\nUrlInput.displayName = \"UrlInput\";\n\n/* ================================================================== */\n/*  EXPORTS                                                            */\n/* ================================================================== */\n\nexport {\n  applyInputMask,\n  MaskedInput,\n  NumberInput,\n  SearchInput,\n  PhoneInput,\n  PasswordInput,\n  UrlInput,\n};\n",
      "type": "registry:ui",
      "target": "@ui/atoms/form-fields.tsx"
    }
  ],
  "meta": {
    "category": "phase-5-foundation",
    "phase": "beta"
  },
  "type": "registry:ui"
}