{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "number-input",
  "type": "registry:ui",
  "title": "NumberInput",
  "description": "ورودی عدد با دکمه‌های + و −؛ ↑↓ یک گام، PageUp/PageDown ده گام، Home/End کمینه و بیشینه",
  "dependencies": [
    "@kashi-ui/persian@^1.8.0"
  ],
  "registryDependencies": [
    "https://kashi.sepantapouya.com/r/cx.json",
    "https://kashi.sepantapouya.com/r/field.json",
    "https://kashi.sepantapouya.com/r/kashi-provider.json"
  ],
  "files": [
    {
      "path": "registry/kashi/NumberInput.tsx",
      "type": "registry:ui",
      "target": "components/kashi/NumberInput.tsx",
      "content": "\"use client\";\n\nimport { forwardRef, useId, useState, type KeyboardEvent, type ReactNode } from \"react\";\nimport { formatNumber, onlyDigits, toFaDigits } from \"@kashi-ui/persian\";\nimport { cx } from \"./cx\";\nimport { Field, describedBy } from \"./Field\";\nimport { useKashi } from \"./KashiProvider\";\n\nexport interface NumberInputProps {\n  label?: ReactNode;\n  hint?: ReactNode;\n  error?: ReactNode;\n  value?: number | null;\n  defaultValue?: number | null;\n  onChange?: (value: number | null) => void;\n  min?: number;\n  max?: number;\n  /** @default 1 */\n  step?: number;\n  /** جداکنندهٔ هزارگان */\n  thousands?: boolean;\n  /** @default \"fa\" */\n  digits?: \"fa\" | \"latin\";\n  /** واحد کنار عدد، مثل «نفر» */\n  unit?: ReactNode;\n  disabled?: boolean;\n  name?: string;\n  id?: string;\n  className?: string;\n  labels?: { increment?: string; decrement?: string };\n}\n\nconst Minus = () => <svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" strokeWidth={1.8} strokeLinecap=\"round\" aria-hidden=\"true\"><path d=\"M5 10h10\" /></svg>;\nconst Plus = () => <svg viewBox=\"0 0 20 20\" fill=\"none\" stroke=\"currentColor\" strokeWidth={1.8} strokeLinecap=\"round\" aria-hidden=\"true\"><path d=\"M10 5v10M5 10h10\" /></svg>;\n\n/** ورودی عدد با دکمه‌های + و −؛ ↑↓ یک گام، PageUp/PageDown ده گام، Home/End کمینه و بیشینه */\nexport const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(function NumberInput(\n  { label, hint, error, value, defaultValue = null, onChange, min, max, step = 1, thousands, digits, unit, disabled, name, id, className, labels = {} },\n  ref,\n) {\n  const { messages: M, digits: D } = useKashi();\n  digits ??= D;\n  const autoId = useId();\n  const inputId = id ?? autoId;\n  const [inner, setInner] = useState<number | null>(defaultValue);\n  const current = value !== undefined ? value : inner;\n  const [draft, setDraft] = useState<string | null>(null);\n  const { increment = M.numberInput.increment, decrement = M.numberInput.decrement } = labels;\n\n  const clamp = (n: number) => Math.min(max ?? Infinity, Math.max(min ?? -Infinity, n));\n  const show = (n: number | null) => {\n    if (n === null) return \"\";\n    const s = thousands ? formatNumber(n, { digits: \"latin\" }) : String(n);\n    return digits === \"fa\" ? toFaDigits(s).replace(/,/g, \"٬\") : s;\n  };\n  const set = (n: number | null) => {\n    // هنگام تایپ محدود نمی‌شود تا عدد نیمه‌کاره پاک نشود؛ محدودسازی در blur و دکمه‌ها\n    if (value === undefined) setInner(n);\n    onChange?.(n);\n  };\n  const bump = (k: number) => set(clamp((current ?? min ?? 0) + k * step));\n\n  const onKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {\n    const keys: Record<string, () => void> = {\n      ArrowUp: () => bump(1),\n      ArrowDown: () => bump(-1),\n      PageUp: () => bump(10),\n      PageDown: () => bump(-10),\n      Home: () => min !== undefined && set(min),\n      End: () => max !== undefined && set(max),\n    };\n    if (!keys[e.key]) return;\n    e.preventDefault();\n    keys[e.key]();\n    setDraft(null);\n  };\n\n  return (\n    <Field id={inputId} label={label} hint={hint} error={error} className={className}>\n      <div className={cx(\"k-number\")}>\n        <div className=\"k-input\" data-state={error ? \"invalid\" : disabled ? \"disabled\" : undefined}>\n          <button type=\"button\" className=\"k-number__step\" tabIndex={-1} aria-label={decrement} disabled={disabled || (min !== undefined && (current ?? 0) <= min)} onClick={() => bump(-1)}><Minus /></button>\n          <input\n            ref={ref}\n            id={inputId}\n            className=\"k-input__control\"\n            type=\"text\"\n            inputMode=\"decimal\"\n            role=\"spinbutton\"\n            aria-valuenow={current ?? undefined}\n            aria-valuemin={min}\n            aria-valuemax={max}\n            aria-valuetext={current === null ? undefined : show(current)}\n            aria-invalid={error ? true : undefined}\n            aria-describedby={describedBy(inputId, hint, error)}\n            disabled={disabled}\n            dir=\"ltr\"\n            value={draft ?? show(current)}\n            onChange={(e) => {\n              const raw = e.target.value;\n              const neg = /^\\s*[-−]/.test(raw);\n              const d = onlyDigits(raw);\n              setDraft(raw);\n              set(d ? (neg ? -1 : 1) * Number(d) : null);\n            }}\n            onBlur={() => {\n              setDraft(null);\n              if (current !== null && current !== clamp(current)) set(clamp(current));\n            }}\n            onKeyDown={onKeyDown}\n          />\n          {unit && <span className=\"k-input__addon\">{unit}</span>}\n          <button type=\"button\" className=\"k-number__step\" tabIndex={-1} aria-label={increment} disabled={disabled || (max !== undefined && (current ?? 0) >= max)} onClick={() => bump(1)}><Plus /></button>\n        </div>\n      </div>\n      {name && <input type=\"hidden\" name={name} value={current ?? \"\"} />}\n    </Field>\n  );\n});\n"
    }
  ]
}