{
  "$schema": "https://cubixflow.ir/schema/registry-item.json",
  "name": "number-field",
  "type": "registry:ui",
  "title": "Number Field",
  "description": "A labeled number field with icons, stepper arrows, description, and error. inputMode is locked to numeric; values always display as Persian digits.",
  "dependencies": [
    "@base-ui/react"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "components/cubix/number-field.tsx",
      "type": "registry:ui",
      "target": "components/cubix/number-field.tsx",
      "content": "\"use client\"\n\n/*\n  Cubix Number Field - labeled integer input specialized from Text Field.\n\n  type stays text with inputMode numeric; values always display as Persian\n  digits. text-right keeps values readable in RTL. Stepper arrows and\n  ArrowUp / ArrowDown on the input adjust the value. Weight 400 and tracking-normal keep IRANSans XV readable. Icons use\n  data-icon like Button.\n*/\nimport * as React from \"react\"\nimport { Field as FieldPrimitive } from \"@base-ui/react/field\"\nimport { Input as InputPrimitive } from \"@base-ui/react/input\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\n/*\n  Icon-side inline padding matches Button / vertical icon inset:\n  default: 8px · lg: 12px\n*/\n\nconst numberFieldControlVariants = cva(\n  \"group/number-field-control relative flex w-full min-w-0 items-center rounded-lg border border-input bg-transparent transition-colors outline-none has-disabled:cursor-not-allowed has-disabled:border-transparent has-disabled:bg-muted has-[[data-slot=number-field-input]:focus-visible]:border-primary has-[[data-slot=number-field-input]:focus-visible]:ring-3 has-[[data-slot=number-field-input]:focus-visible]:ring-primary/20 has-[[data-slot=number-field-input]:focus-visible]:ring-offset-1 has-[[data-slot=number-field-input]:focus-visible]:ring-offset-background has-[[data-slot=number-field-input][aria-invalid=true]]:border-destructive has-[[data-slot=number-field-input][aria-invalid=true]]:ring-0 has-[[data-slot=number-field-input][aria-invalid=true]:focus-visible]:border-destructive has-[[data-slot=number-field-input][aria-invalid=true]:focus-visible]:ring-0 dark:bg-input/30 dark:has-disabled:bg-muted dark:has-[[data-slot=number-field-input][aria-invalid=true]]:border-destructive/50 dark:has-[[data-slot=number-field-input][aria-invalid=true]:focus-visible]:border-destructive/50 dark:has-[[data-slot=number-field-input][aria-invalid=true]:focus-visible]:ring-0 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&>svg]:text-muted-foreground [&_[data-icon]]:text-muted-foreground [&_svg:not([class*='size-'])]:size-4\",\n  {\n    variants: {\n      size: {\n        default:\n          \"h-10 gap-2 px-3 has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2 [&_svg:not([class*='size-'])]:size-6\",\n        lg: \"h-12 gap-2.5 px-3.5 has-data-[icon=inline-end]:pe-3 has-data-[icon=inline-start]:ps-3 [&_svg:not([class*='size-'])]:size-6\",\n      },\n    },\n    defaultVariants: {\n      size: \"default\",\n    },\n  }\n)\n\nconst numberFieldInputVariants = cva(\n  \"w-full min-w-0 rounded-lg border border-input bg-transparent text-right font-normal leading-none tracking-normal text-foreground transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-description file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-primary focus-visible:ring-3 focus-visible:ring-primary/20 focus-visible:ring-offset-1 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:border-transparent disabled:bg-muted disabled:text-muted-foreground disabled:placeholder:text-muted-foreground aria-invalid:border-destructive aria-invalid:ring-0 aria-invalid:focus-visible:border-destructive aria-invalid:focus-visible:ring-0 dark:bg-input/30 dark:disabled:bg-muted dark:aria-invalid:border-destructive/50 dark:aria-invalid:focus-visible:border-destructive/50\",\n  {\n    variants: {\n      size: {\n        default: \"h-10 px-3 text-label md:text-label\",\n        lg: \"h-12 px-3.5 text-label md:text-label\",\n      },\n      inControl: {\n        true: \"h-full flex-1 rounded-none border-0 bg-transparent p-0 shadow-none ring-0 focus-visible:border-0 focus-visible:ring-0 focus-visible:ring-offset-0 aria-invalid:border-0 aria-invalid:ring-0 aria-invalid:ring-offset-0 disabled:bg-transparent dark:bg-transparent dark:disabled:bg-transparent\",\n        false: \"\",\n      },\n    },\n    defaultVariants: {\n      size: \"default\",\n      inControl: false,\n    },\n  }\n)\n\ntype NumberFieldSize = NonNullable<VariantProps<typeof numberFieldInputVariants>[\"size\"]>\n\nconst NumberFieldSizeContext = React.createContext<NumberFieldSize>(\"default\")\nconst NumberFieldControlContext = React.createContext(false)\n\nfunction NumberField({\n  className,\n  size = \"default\",\n  ...props\n}: React.ComponentProps<typeof FieldPrimitive.Root> & {\n  size?: NumberFieldSize\n}) {\n  return (\n    <NumberFieldSizeContext.Provider value={size}>\n      <FieldPrimitive.Root\n        data-slot=\"number-field\"\n        data-size={size}\n        className={cn(\n          \"group/number-field flex w-full flex-col gap-2 data-disabled:cursor-not-allowed data-disabled:opacity-70\",\n          className\n        )}\n        {...props}\n      />\n    </NumberFieldSizeContext.Provider>\n  )\n}\n\nfunction NumberFieldLabel({\n  className,\n  ...props\n}: React.ComponentProps<typeof FieldPrimitive.Label>) {\n  return (\n    <FieldPrimitive.Label\n      data-slot=\"number-field-label\"\n      className={cn(\n        \"flex w-fit items-center gap-2 text-label leading-none font-normal text-foreground select-none group-data-disabled/number-field:cursor-not-allowed group-data-disabled/number-field:text-muted-foreground\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction NumberFieldControl({\n  className,\n  size: sizeProp,\n  onClick,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  size?: NumberFieldSize\n}) {\n  const contextSize = React.useContext(NumberFieldSizeContext)\n  const size = sizeProp ?? contextSize\n\n  return (\n    <NumberFieldControlContext.Provider value={true}>\n      <div\n        data-slot=\"number-field-control\"\n        data-size={size}\n        className={cn(numberFieldControlVariants({ size }), className)}\n        onClick={(event) => {\n          onClick?.(event)\n          if (event.defaultPrevented) return\n          if ((event.target as HTMLElement).closest(\"button,a,input,textarea\")) {\n            return\n          }\n          const input = event.currentTarget.querySelector<HTMLInputElement>(\n            \"[data-slot=number-field-input]\"\n          )\n          if (!input || input.disabled || input.readOnly) return\n          input.focus()\n        }}\n        {...props}\n      />\n    </NumberFieldControlContext.Provider>\n  )\n}\n\nfunction NumberFieldInput({\n  className,\n  size: sizeProp,\n  autoComplete = \"off\",\n  onBeforeInput,\n  onPaste,\n  onInput,\n  onKeyDown,\n  defaultValue,\n  value,\n  ...props\n}: Omit<\n  React.ComponentProps<typeof InputPrimitive>,\n  \"size\" | \"type\" | \"inputMode\" | \"spellCheck\"\n> & {\n  size?: NumberFieldSize\n}) {\n  const contextSize = React.useContext(NumberFieldSizeContext)\n  const size = sizeProp ?? contextSize\n  const inControl = React.useContext(NumberFieldControlContext)\n  const persianDefault =\n    defaultValue == null\n      ? defaultValue\n      : toPersianDigits(String(defaultValue).replace(/[^\\d۰-۹٠-٩.\\-]/g, \"\"))\n  const persianValue =\n    value === undefined\n      ? undefined\n      : value == null\n        ? value\n        : toPersianDigits(String(value).replace(/[^\\d۰-۹٠-٩.\\-]/g, \"\"))\n\n  return (\n    <InputPrimitive\n      data-slot=\"number-field-input\"\n      data-size={size}\n      className={cn(numberFieldInputVariants({ size, inControl }), className)}\n      {...props}\n      type=\"text\"\n      inputMode=\"numeric\"\n      autoComplete={autoComplete}\n      spellCheck={false}\n      defaultValue={persianDefault}\n      value={persianValue}\n      onBeforeInput={(event) => {\n        onBeforeInput?.(event)\n        if (event.defaultPrevented) return\n        if (typeof event.data !== \"string\" || event.data.length === 0) return\n        if (/[^\\d۰-۹٠-٩.\\-]/.test(event.data)) {\n          event.preventDefault()\n          return\n        }\n        const persian = toPersianDigits(event.data)\n        if (persian === event.data) return\n        event.preventDefault()\n        const input = event.currentTarget\n        const start = input.selectionStart ?? input.value.length\n        const end = input.selectionEnd ?? input.value.length\n        const next = input.value.slice(0, start) + persian + input.value.slice(end)\n        setNumberFieldInputValue(input, next)\n        const caret = start + persian.length\n        requestAnimationFrame(() => {\n          input.setSelectionRange(caret, caret)\n        })\n      }}\n      onPaste={(event) => {\n        onPaste?.(event)\n        if (event.defaultPrevented) return\n        const text = event.clipboardData?.getData(\"text\") ?? \"\"\n        const sanitized = toPersianDigits(text.replace(/[^\\d۰-۹٠-٩.\\-]/g, \"\"))\n        if (!sanitized) {\n          if (/[^\\d۰-۹٠-٩.\\-]/.test(text) || /[\\d٠-٩]/.test(text)) {\n            event.preventDefault()\n          }\n          return\n        }\n        if (sanitized === text) return\n        event.preventDefault()\n        const input = event.currentTarget\n        const start = input.selectionStart ?? input.value.length\n        const end = input.selectionEnd ?? input.value.length\n        const next = input.value.slice(0, start) + sanitized + input.value.slice(end)\n        setNumberFieldInputValue(input, next)\n        const caret = start + sanitized.length\n        requestAnimationFrame(() => {\n          input.setSelectionRange(caret, caret)\n        })\n      }}\n      onKeyDown={(event) => {\n        onKeyDown?.(event)\n        if (event.defaultPrevented) return\n        if (event.key !== \"ArrowUp\" && event.key !== \"ArrowDown\") return\n        event.preventDefault()\n        stepNumberFieldInput(event.currentTarget, event.key === \"ArrowUp\" ? 1 : -1)\n      }}\n      onInput={(event) => {\n        onInput?.(event)\n        if (event.defaultPrevented || value !== undefined) return\n        const input = event.currentTarget\n        const next = toPersianDigits(input.value.replace(/[^\\d۰-۹٠-٩.\\-]/g, \"\"))\n        if (next === input.value) return\n        const caret = input.selectionStart ?? next.length\n        setNumberFieldInputValue(input, next)\n        requestAnimationFrame(() => {\n          const pos = Math.min(caret, next.length)\n          input.setSelectionRange(pos, pos)\n        })\n      }}\n    />\n  )\n}\n\nconst PERSIAN_DIGITS = \"۰۱۲۳۴۵۶۷۸۹\"\nconst ARABIC_DIGITS = \"٠١٢٣٤٥٦٧٨٩\"\n\nfunction toLatinDigits(value: string) {\n  return value\n    .replace(/[۰-۹]/g, (digit) => String(PERSIAN_DIGITS.indexOf(digit)))\n    .replace(/[٠-٩]/g, (digit) => String(ARABIC_DIGITS.indexOf(digit)))\n}\n\nfunction toPersianDigits(value: string) {\n  return toLatinDigits(value).replace(/\\d/g, (digit) => PERSIAN_DIGITS[Number(digit)] ?? digit)\n}\n\nfunction formatDigits(value: number) {\n  return toPersianDigits(String(value))\n}\n\nfunction setNumberFieldInputValue(input: HTMLInputElement, next: string) {\n  const descriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, \"value\")\n  descriptor?.set?.call(input, next)\n  input.dispatchEvent(new Event(\"input\", { bubbles: true }))\n  input.dispatchEvent(new Event(\"change\", { bubbles: true }))\n}\n\nfunction stepNumberFieldInput(input: HTMLInputElement, direction: 1 | -1, stepOverride?: number) {\n  if (input.disabled || input.readOnly) return\n\n  const latin = toLatinDigits(input.value).replace(/[^\\d.-]/g, \"\")\n  const current = latin === \"\" || latin === \"-\" || latin === \".\" ? 0 : Number(latin)\n  if (Number.isNaN(current)) return\n\n  const stepAttr = input.step\n  const parsedStep = stepOverride ?? (stepAttr && stepAttr !== \"any\" ? Number(stepAttr) : 1)\n  const step = Number.isFinite(parsedStep) && parsedStep !== 0 ? parsedStep : 1\n\n  let next = current + direction * step\n  const min = input.min === \"\" ? undefined : Number(input.min)\n  const max = input.max === \"\" ? undefined : Number(input.max)\n  if (min !== undefined && Number.isFinite(min)) next = Math.max(min, next)\n  if (max !== undefined && Number.isFinite(max)) next = Math.min(max, next)\n  if (Number.isInteger(step) && Number.isInteger(current)) {\n    next = Math.trunc(next)\n  }\n\n  setNumberFieldInputValue(input, formatDigits(next))\n}\n\nfunction NumberFieldChevronUpIcon({ className }: { className?: string }) {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      aria-hidden=\"true\"\n      className={className}\n    >\n      <path\n        d=\"M6 14.5L12 8.5L18 14.5\"\n        stroke=\"currentColor\"\n        strokeWidth=\"1.5\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  )\n}\n\nfunction NumberFieldChevronDownIcon({ className }: { className?: string }) {\n  return (\n    <svg\n      xmlns=\"http://www.w3.org/2000/svg\"\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      aria-hidden=\"true\"\n      className={className}\n    >\n      <path\n        d=\"M6 9.5L12 15.5L18 9.5\"\n        stroke=\"currentColor\"\n        strokeWidth=\"1.5\"\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  )\n}\n\nfunction NumberFieldStepper({\n  className,\n  step,\n  incrementLabel = \"افزایش\",\n  decrementLabel = \"کاهش\",\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  step?: number\n  incrementLabel?: string\n  decrementLabel?: string\n}) {\n  const ref = React.useRef<HTMLDivElement>(null)\n  const [disabled, setDisabled] = React.useState(false)\n\n  React.useLayoutEffect(() => {\n    const control = ref.current?.closest(\"[data-slot=number-field-control]\")\n    const input = control?.querySelector<HTMLInputElement>(\"[data-slot=number-field-input]\")\n    if (!input) return\n\n    const sync = () => {\n      setDisabled(input.disabled || input.readOnly)\n    }\n    sync()\n    input.addEventListener(\"input\", sync)\n    const observer = new MutationObserver(sync)\n    observer.observe(input, {\n      attributes: true,\n      attributeFilter: [\"disabled\", \"readonly\"],\n    })\n    return () => {\n      input.removeEventListener(\"input\", sync)\n      observer.disconnect()\n    }\n  }, [])\n\n  const runStep = (direction: 1 | -1) => {\n    const input = ref.current\n      ?.closest(\"[data-slot=number-field-control]\")\n      ?.querySelector<HTMLInputElement>(\"[data-slot=number-field-input]\")\n    if (!input) return\n    stepNumberFieldInput(input, direction, step)\n    input.focus()\n  }\n\n  return (\n    <div\n      ref={ref}\n      data-slot=\"number-field-stepper\"\n      data-icon=\"inline-end\"\n      className={cn(\"flex shrink-0 flex-col items-center justify-center -space-y-1\", className)}\n      {...props}\n    >\n      <button\n        type=\"button\"\n        data-slot=\"number-field-increment\"\n        aria-label={incrementLabel}\n        disabled={disabled}\n        tabIndex={-1}\n        className=\"inline-flex size-4 items-center justify-center rounded-sm text-muted-foreground outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:bg-muted focus-visible:text-foreground focus-visible:ring-2 focus-visible:ring-primary/20 disabled:pointer-events-none disabled:opacity-50 [&_svg]:text-current\"\n        onMouseDown={(event) => event.preventDefault()}\n        onClick={() => runStep(1)}\n      >\n        <NumberFieldChevronUpIcon className=\"size-3.5\" />\n      </button>\n      <button\n        type=\"button\"\n        data-slot=\"number-field-decrement\"\n        aria-label={decrementLabel}\n        disabled={disabled}\n        tabIndex={-1}\n        className=\"inline-flex size-4 items-center justify-center rounded-sm text-muted-foreground outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:bg-muted focus-visible:text-foreground focus-visible:ring-2 focus-visible:ring-primary/20 disabled:pointer-events-none disabled:opacity-50 [&_svg]:text-current\"\n        onMouseDown={(event) => event.preventDefault()}\n        onClick={() => runStep(-1)}\n      >\n        <NumberFieldChevronDownIcon className=\"size-3.5\" />\n      </button>\n    </div>\n  )\n}\n\nfunction NumberFieldDescription({\n  className,\n  ...props\n}: React.ComponentProps<typeof FieldPrimitive.Description>) {\n  return (\n    <FieldPrimitive.Description\n      data-slot=\"number-field-description\"\n      className={cn(\n        \"m-0 text-caption font-normal text-muted-foreground group-data-disabled/number-field:cursor-not-allowed\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction NumberFieldError({\n  className,\n  children,\n  match,\n  ...props\n}: React.ComponentProps<typeof FieldPrimitive.Error>) {\n  return (\n    <FieldPrimitive.Validity>\n      {(validity) => {\n        const isInvalid = validity.validity.valid === false\n        if (!isInvalid && match !== true) {\n          return null\n        }\n\n        return (\n          <FieldPrimitive.Error\n            data-slot=\"number-field-error\"\n            match={match ?? true}\n            className={cn(\"m-0 text-caption font-normal text-destructive\", className)}\n            {...props}\n          >\n            {children}\n          </FieldPrimitive.Error>\n        )\n      }}\n    </FieldPrimitive.Validity>\n  )\n}\n\nexport {\n  NumberField,\n  NumberFieldLabel,\n  NumberFieldControl,\n  NumberFieldInput,\n  NumberFieldStepper,\n  NumberFieldDescription,\n  NumberFieldError,\n  numberFieldControlVariants,\n  numberFieldInputVariants,\n}\n"
    }
  ]
}

