{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "number",
  "title": "NumberField",
  "description": "Number input field for BuzzForm v2.",
  "registryDependencies": [
    "@buzzform/init",
    "input",
    "input-group",
    "field",
    "@buzzform/copy"
  ],
  "files": [
    {
      "path": "registry/shadcn/fields/number.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport type { NumberField as NumberFieldDef } from \"@buildnbuzz/form-react\";\nimport {\n  clampNumber,\n  applyNumericPrecision,\n  formatNumberWithSeparator,\n  parseFormattedNumber,\n} from \"@buildnbuzz/form-react\";\nimport { useDataField } from \"@buildnbuzz/form-react\";\nimport { Input } from \"@/components/ui/input\";\nimport { CopyButton } from \"../components/copy-button\";\nimport {\n  InputGroup,\n  InputGroupAddon,\n  InputGroupButton,\n  InputGroupInput,\n} from \"@/components/ui/input-group\";\nimport {\n  Field,\n  FieldContent,\n  FieldDescription,\n  FieldError,\n  FieldGroup,\n  FieldLabel,\n} from \"@/components/ui/field\";\nimport { IconPlaceholder } from \"@/components/icon-placeholder\";\nimport { cn } from \"@/lib/utils\";\n\ninterface NumberUi {\n  prefix?: string;\n  suffix?: string;\n  variant?: \"default\" | \"stacked\" | \"pill\" | \"plain\";\n  hideSteppers?: boolean;\n  thousandSeparator?: boolean | string;\n  copyable?: boolean;\n  autoFocus?: boolean;\n  className?: string;\n  width?: string | number;\n  asterisk?: boolean;\n}\n\nexport function NumberField() {\n  const {\n    fieldApi,\n    field,\n    isDisabled,\n    isReadOnly,\n    isRequired,\n    label,\n    placeholder,\n    description,\n    errors,\n    isInvalid,\n    descriptionId,\n    errorId,\n    ariaDescribedBy,\n    handleBlur,\n  } = useDataField<NumberFieldDef>();\n\n  const value = fieldApi.state.value as number | undefined;\n  const ui = field.ui as NumberUi | undefined;\n\n  // UI options\n  const step = field.step ?? 1;\n  const variant = ui?.variant ?? \"default\";\n  const prefix = ui?.prefix;\n  const suffix = ui?.suffix;\n  const hideSteppers = ui?.hideSteppers || variant === \"plain\";\n  const thousandSeparator = ui?.thousandSeparator;\n  const copyable = ui?.copyable;\n  const sep = typeof thousandSeparator === \"string\" ? thousandSeparator : \",\";\n\n  const width = ui?.width;\n  const widthStyle = width\n    ? {\n        width: typeof width === \"number\" ? `${width}px` : width,\n        flex: \"0 1 auto\",\n      }\n    : undefined;\n\n  // Display value for thousand separator mode\n  const [displayValue, setDisplayValue] = React.useState<string>(() =>\n    thousandSeparator && typeof value === \"number\"\n      ? formatNumberWithSeparator(value, sep)\n      : \"\",\n  );\n\n  // Sync display value when external value changes\n  React.useEffect(() => {\n    if (thousandSeparator && typeof value === \"number\") {\n      setDisplayValue(formatNumberWithSeparator(value, sep));\n    } else if (thousandSeparator && value === undefined) {\n      setDisplayValue(\"\");\n    }\n  }, [value, thousandSeparator, sep]);\n\n  const handleChange = (val: number | undefined) => {\n    if (isReadOnly) return;\n    // Apply precision if specified\n    const finalVal = applyNumericPrecision(val, field.precision);\n    fieldApi.handleChange(finalVal);\n  };\n\n  const handleAdjust = (delta: number) => {\n    if (isReadOnly || isDisabled) return;\n    const current = typeof value === \"number\" ? value : (field.min ?? 0);\n    const next = clampNumber(current + delta, field.min, field.max);\n    handleChange(next);\n  };\n\n  const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n    if (isDisabled || isReadOnly) return;\n    if (e.key === \"ArrowUp\") {\n      e.preventDefault();\n      handleAdjust(step);\n    } else if (e.key === \"ArrowDown\") {\n      e.preventDefault();\n      handleAdjust(-step);\n    }\n  };\n\n  const handleFormattedInputChange = (\n    e: React.ChangeEvent<HTMLInputElement>,\n  ) => {\n    const inputVal = e.target.value;\n    setDisplayValue(inputVal);\n    const parsed = parseFormattedNumber(inputVal, sep);\n    handleChange(parsed);\n  };\n\n  const handleFormattedInputBlur = () => {\n    if (typeof value === \"number\") {\n      setDisplayValue(formatNumberWithSeparator(value, sep));\n    }\n    handleBlur();\n  };\n\n  const renderInput = (className?: string) => {\n    // When thousandSeparator is enabled, use text input\n    if (thousandSeparator) {\n      return (\n        <InputGroupInput\n          id={fieldApi.name}\n          type=\"text\"\n          inputMode=\"numeric\"\n          value={displayValue}\n          onChange={handleFormattedInputChange}\n          onBlur={handleFormattedInputBlur}\n          onKeyDown={handleKeyDown}\n          placeholder={placeholder}\n          className={cn(\n            hideSteppers ? \"\" : \"text-center\",\n            \"tabular-nums\",\n            className,\n          )}\n          disabled={isDisabled}\n          readOnly={isReadOnly}\n          aria-invalid={isInvalid}\n          aria-describedby={ariaDescribedBy}\n          autoFocus={ui?.autoFocus}\n        />\n      );\n    }\n\n    // Standard number input\n    return (\n      <InputGroupInput\n        id={fieldApi.name}\n        name={fieldApi.name}\n        type=\"number\"\n        value={value ?? \"\"}\n        onChange={(e: React.ChangeEvent<HTMLInputElement>) =>\n          handleChange(\n            e.target.value === \"\" ? undefined : Number(e.target.value),\n          )\n        }\n        onBlur={handleBlur}\n        onKeyDown={handleKeyDown}\n        placeholder={placeholder}\n        className={cn(\n          hideSteppers\n            ? \"\"\n            : \"text-center [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none [-moz-appearance:textfield]\",\n          className,\n        )}\n        disabled={isDisabled}\n        readOnly={isReadOnly}\n        min={field.min}\n        max={field.max}\n        step={step}\n        aria-invalid={isInvalid}\n        aria-describedby={ariaDescribedBy}\n        autoFocus={ui?.autoFocus}\n      />\n    );\n  };\n\n  const renderInputContent = () => {\n    // Plain variant - no steppers\n    if (hideSteppers) {\n      if (prefix || suffix || copyable) {\n        return (\n          <InputGroup className=\"h-9\">\n            {prefix && (\n              <InputGroupAddon\n                align=\"inline-start\"\n                className=\"px-3 text-muted-foreground text-sm\"\n              >\n                {prefix}\n              </InputGroupAddon>\n            )}\n            {renderInput()}\n            {suffix && (\n              <InputGroupAddon className=\"px-3 text-muted-foreground text-sm\">\n                {suffix}\n              </InputGroupAddon>\n            )}\n            {copyable && (\n              <InputGroupAddon align=\"inline-end\">\n                <CopyButton value={value?.toString() ?? \"\"} />\n              </InputGroupAddon>\n            )}\n          </InputGroup>\n        );\n      }\n      return (\n        <Input\n          id={fieldApi.name}\n          type=\"number\"\n          value={value ?? \"\"}\n          onChange={(e) =>\n            handleChange(\n              e.target.value === \"\" ? undefined : Number(e.target.value),\n            )\n          }\n          onBlur={handleBlur}\n          onKeyDown={handleKeyDown}\n          placeholder={placeholder}\n          disabled={isDisabled}\n          readOnly={isReadOnly}\n          min={field.min}\n          max={field.max}\n          step={step}\n          aria-invalid={isInvalid}\n          aria-describedby={ariaDescribedBy}\n          autoFocus={ui?.autoFocus}\n          className=\"tabular-nums\"\n        />\n      );\n    }\n\n    // Stacked variant - vertical buttons on right\n    if (variant === \"stacked\") {\n      return (\n        <InputGroup className=\"h-9\">\n          {prefix && (\n            <InputGroupAddon\n              align=\"inline-start\"\n              className=\"px-3 text-muted-foreground text-sm\"\n            >\n              {prefix}\n            </InputGroupAddon>\n          )}\n          {renderInput(\"tabular-nums\")}\n          {suffix && (\n            <InputGroupAddon className=\"px-2 text-muted-foreground text-sm\">\n              {suffix}\n            </InputGroupAddon>\n          )}\n          <InputGroupAddon\n            align=\"inline-end\"\n            className=\"p-0 pr-0 h-full self-stretch\"\n          >\n            <div className=\"flex flex-col h-full border-l border-input -mr-px overflow-hidden rounded-r-md\">\n              <button\n                type=\"button\"\n                onClick={() => handleAdjust(step)}\n                disabled={isDisabled || isReadOnly}\n                className=\"flex-1 w-7 flex items-center justify-center border-b border-input text-muted-foreground hover:bg-accent hover:text-foreground transition-colors disabled:pointer-events-none disabled:opacity-50\"\n                aria-label=\"Increment\"\n              >\n                <IconPlaceholder\n                  lucide=\"Plus\"\n                  hugeicons=\"Add01Icon\"\n                  tabler=\"IconPlus\"\n                  phosphor=\"Plus\"\n                  remixicon=\"RiAddLine\"\n                  className=\"size-3\"\n                />\n              </button>\n              <button\n                type=\"button\"\n                onClick={() => handleAdjust(-step)}\n                disabled={isDisabled || isReadOnly}\n                className=\"flex-1 w-7 flex items-center justify-center text-muted-foreground hover:bg-accent hover:text-foreground transition-colors disabled:pointer-events-none disabled:opacity-50\"\n                aria-label=\"Decrement\"\n              >\n                <IconPlaceholder\n                  lucide=\"Minus\"\n                  hugeicons=\"MinusSignIcon\"\n                  tabler=\"IconMinus\"\n                  phosphor=\"Minus\"\n                  remixicon=\"RiSubtractLine\"\n                  className=\"size-3\"\n                />\n              </button>\n            </div>\n          </InputGroupAddon>\n        </InputGroup>\n      );\n    }\n\n    // Pill variant - rounded with buttons on sides\n    if (variant === \"pill\") {\n      return (\n        <InputGroup className=\"h-9 rounded-full px-1.5\">\n          <InputGroupAddon align=\"inline-start\">\n            <InputGroupButton\n              variant=\"ghost\"\n              size=\"icon-xs\"\n              onClick={() => handleAdjust(-step)}\n              className=\"rounded-full size-6\"\n              disabled={isDisabled || isReadOnly}\n              aria-label=\"Decrement\"\n            >\n              <IconPlaceholder\n                lucide=\"Minus\"\n                hugeicons=\"MinusSignIcon\"\n                tabler=\"IconMinus\"\n                phosphor=\"Minus\"\n                remixicon=\"RiSubtractLine\"\n                className=\"size-4\"\n              />\n            </InputGroupButton>\n          </InputGroupAddon>\n          {prefix && (\n            <span className=\"text-muted-foreground text-sm\">{prefix}</span>\n          )}\n          {renderInput()}\n          {suffix && (\n            <span className=\"text-muted-foreground text-sm\">{suffix}</span>\n          )}\n          <InputGroupAddon align=\"inline-end\">\n            <InputGroupButton\n              variant=\"ghost\"\n              size=\"icon-xs\"\n              onClick={() => handleAdjust(step)}\n              className=\"rounded-full size-6\"\n              disabled={isDisabled || isReadOnly}\n              aria-label=\"Increment\"\n            >\n              <IconPlaceholder\n                lucide=\"Plus\"\n                hugeicons=\"Add01Icon\"\n                tabler=\"IconPlus\"\n                phosphor=\"Plus\"\n                remixicon=\"RiAddLine\"\n                className=\"size-4\"\n              />\n            </InputGroupButton>\n          </InputGroupAddon>\n        </InputGroup>\n      );\n    }\n\n    // Default variant - inline buttons on sides\n    return (\n      <InputGroup className=\"h-9\">\n        <InputGroupAddon align=\"inline-start\" className=\"pl-1\">\n          <InputGroupButton\n            variant=\"ghost\"\n            size=\"icon-xs\"\n            onClick={() => handleAdjust(-step)}\n            disabled={isDisabled || isReadOnly}\n            aria-label=\"Decrement\"\n            className=\"size-7\"\n          >\n            <IconPlaceholder\n              lucide=\"Minus\"\n              hugeicons=\"MinusSignIcon\"\n              tabler=\"IconMinus\"\n              phosphor=\"Minus\"\n              remixicon=\"RiSubtractLine\"\n              className=\"size-3.5\"\n            />\n          </InputGroupButton>\n        </InputGroupAddon>\n        {prefix && (\n          <span className=\"text-muted-foreground text-sm pl-1\">{prefix}</span>\n        )}\n        {renderInput()}\n        {suffix && (\n          <span className=\"text-muted-foreground text-sm pr-1\">{suffix}</span>\n        )}\n        <InputGroupAddon align=\"inline-end\" className=\"pr-1\">\n          <InputGroupButton\n            variant=\"ghost\"\n            size=\"icon-xs\"\n            onClick={() => handleAdjust(step)}\n            disabled={isDisabled || isReadOnly}\n            aria-label=\"Increment\"\n            className=\"size-7\"\n          >\n            <IconPlaceholder\n              lucide=\"Plus\"\n              hugeicons=\"Add01Icon\"\n              tabler=\"IconPlus\"\n              phosphor=\"Plus\"\n              remixicon=\"RiAddLine\"\n              className=\"size-3.5\"\n            />\n          </InputGroupButton>\n        </InputGroupAddon>\n      </InputGroup>\n    );\n  };\n\n  return (\n    <FieldGroup\n      data-field={fieldApi.name}\n      className={ui?.className}\n      style={widthStyle}\n    >\n      <Field data-invalid={isInvalid} data-disabled={isDisabled}>\n        {label && (\n          <FieldLabel htmlFor={fieldApi.name}>\n            {isRequired && ui?.asterisk !== false && (\n              <span className=\"text-destructive mr-0.5\">*</span>\n            )}\n            {label}\n          </FieldLabel>\n        )}\n\n        <FieldContent>{renderInputContent()}</FieldContent>\n\n        {description && !isInvalid && (\n          <FieldDescription id={descriptionId}>{description}</FieldDescription>\n        )}\n\n        {isInvalid && <FieldError id={errorId} errors={errors} />}\n      </Field>\n    </FieldGroup>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/buzzform/fields/number.tsx"
    }
  ],
  "type": "registry:ui"
}