{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "number-field",
  "title": "Number Field",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-vue/components/number-field/NumberField.vue",
      "content": "<script setup lang=\"ts\">\nimport type { NumberFieldRootEmits, NumberFieldRootProps } from 'reka-ui'\nimport type { HTMLAttributes } from 'vue'\nimport { computed } from 'vue'\nimport { reactiveOmit } from '@vueuse/core'\nimport { NumberFieldRoot, useForwardPropsEmits } from 'reka-ui'\nimport { cn } from '@/lib/utils'\nimport { provideNumberFieldContext } from './NumberFieldContext'\n\nexport type NumberFieldSize = 'small' | 'middle' | 'large'\nexport type NumberFieldStatus = 'error' | 'warning'\nexport type NumberFieldControlsPosition = 'default' | 'right'\n\n// Use intersection in defineProps<> instead of `interface Props extends`:\n// Vue 3.5+'s SFC compiler has its own mini-resolver for the `extends`\n// clause that bails when the external package's package.json lacks\n// `exports.types` (true for reka-ui). The defineProps<T & U>() path\n// delegates type extraction to the project's installed `typescript`\n// package, which is shipped by init.json -- this works reliably.\ninterface ExtraProps {\n  class?: HTMLAttributes['class']\n  size?: NumberFieldSize\n  status?: NumberFieldStatus\n  controlsPosition?: NumberFieldControlsPosition\n  keyboard?: boolean\n  precision?: number\n  formatter?: (value: number | undefined) => string\n  parser?: (displayValue: string) => number | undefined\n  prefix?: string\n  suffix?: string\n}\n\nconst props = withDefaults(defineProps<NumberFieldRootProps & ExtraProps>(), {\n  step: 1,\n  keyboard: true,\n  controlsPosition: 'default',\n})\n\nconst emits = defineEmits<NumberFieldRootEmits>()\n\nconst delegatedProps = reactiveOmit(\n  props,\n  'class',\n  'size',\n  'status',\n  'controlsPosition',\n  'keyboard',\n  'precision',\n  'formatter',\n  'parser',\n  'prefix',\n  'suffix',\n  'formatOptions',\n)\n\nconst formatOptions = computed(() => {\n  if (props.precision !== undefined) {\n    return {\n      ...props.formatOptions,\n      minimumFractionDigits: props.precision,\n      maximumFractionDigits: props.precision,\n    }\n  }\n  return props.formatOptions\n})\n\nconst forwarded = useForwardPropsEmits(delegatedProps, emits)\n\nprovideNumberFieldContext({\n  size: computed(() => props.size ?? 'middle'),\n  status: computed(() => props.status),\n  controlsPosition: computed(() => props.controlsPosition),\n  keyboard: computed(() => props.keyboard),\n  formatter: computed(() => props.formatter),\n  parser: computed(() => props.parser),\n  prefix: computed(() => props.prefix),\n  suffix: computed(() => props.suffix),\n})\n</script>\n\n<template>\n  <NumberFieldRoot\n    v-slot=\"slotProps\"\n    v-bind=\"forwarded\"\n    :format-options=\"formatOptions\"\n    :class=\"cn('inline-flex', props.class)\"\n  >\n    <slot v-bind=\"slotProps\" />\n  </NumberFieldRoot>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/number-field/NumberField.vue"
    },
    {
      "path": "packages/registry-vue/components/number-field/NumberFieldContent.vue",
      "content": "<script setup lang=\"ts\">\nimport type { HTMLAttributes } from 'vue'\nimport { computed } from 'vue'\nimport { cn } from '@/lib/utils'\nimport { injectNumberFieldContext } from './NumberFieldContext'\n\nconst props = defineProps<{\n  class?: HTMLAttributes['class']\n}>()\n\nconst uiContext = injectNumberFieldContext()\n\nconst isRight = computed(() => uiContext.controlsPosition.value === 'right')\n</script>\n\n<template>\n  <div\n    :class=\"\n      cn(\n        'relative',\n        isRight &&\n          'border-input focus-within:ring-ring inline-grid grid-cols-[1fr_auto] grid-rows-[1fr_1fr] items-stretch overflow-hidden rounded-md border focus-within:ring-1',\n        !isRight &&\n          '[&>[data-slot=input]]:has-[[data-slot=decrement]]:pl-5 [&>[data-slot=input]]:has-[[data-slot=increment]]:pr-5',\n        props.class,\n      )\n    \"\n  >\n    <slot />\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/number-field/NumberFieldContent.vue"
    },
    {
      "path": "packages/registry-vue/components/number-field/NumberFieldContext.ts",
      "content": "import type { InjectionKey, Ref } from 'vue'\nimport { inject, provide, ref } from 'vue'\n\nexport type NumberFieldSize = 'small' | 'middle' | 'large'\nexport type NumberFieldStatus = 'error' | 'warning'\nexport type NumberFieldControlsPosition = 'default' | 'right'\n\nexport interface NumberFieldContext {\n  size: Ref<NumberFieldSize>\n  status: Ref<NumberFieldStatus | undefined>\n  controlsPosition: Ref<NumberFieldControlsPosition>\n  keyboard: Ref<boolean>\n  formatter: Ref<((value: number | undefined) => string) | undefined>\n  parser: Ref<((displayValue: string) => number | undefined) | undefined>\n  prefix: Ref<string | undefined>\n  suffix: Ref<string | undefined>\n}\n\nexport const NumberFieldContextKey: InjectionKey<NumberFieldContext> = Symbol('NumberFieldContext')\n\nexport function provideNumberFieldContext(context: NumberFieldContext) {\n  provide(NumberFieldContextKey, context)\n}\n\nexport function injectNumberFieldContext(): NumberFieldContext {\n  return inject(NumberFieldContextKey, {\n    size: ref('middle'),\n    status: ref(undefined),\n    controlsPosition: ref('default'),\n    keyboard: ref(true),\n    formatter: ref(undefined),\n    parser: ref(undefined),\n    prefix: ref(undefined),\n    suffix: ref(undefined),\n  })\n}\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/number-field/NumberFieldContext.ts"
    },
    {
      "path": "packages/registry-vue/components/number-field/NumberFieldDecrement.vue",
      "content": "<script setup lang=\"ts\">\nimport type { NumberFieldDecrementProps } from 'reka-ui'\nimport type { HTMLAttributes } from 'vue'\nimport { computed } from 'vue'\nimport { reactiveOmit } from '@vueuse/core'\nimport { Minus } from 'lucide-vue-next'\nimport { NumberFieldDecrement, useForwardProps } from 'reka-ui'\nimport { cn } from '@/lib/utils'\nimport { injectNumberFieldContext } from './NumberFieldContext'\n\nconst props = defineProps<NumberFieldDecrementProps & { class?: HTMLAttributes['class'] }>()\n\nconst delegatedProps = reactiveOmit(props, 'class')\n\nconst forwarded = useForwardProps(delegatedProps)\n\nconst uiContext = injectNumberFieldContext()\n\nconst isRight = computed(() => uiContext.controlsPosition.value === 'right')\n\nconst iconSize = computed(() => {\n  switch (uiContext.size.value) {\n    case 'small':\n      return 'h-3 w-3'\n    case 'large':\n      return 'h-5 w-5'\n    default:\n      return 'h-4 w-4'\n  }\n})\n</script>\n\n<template>\n  <NumberFieldDecrement\n    data-uipkge\n    data-slot=\"decrement\"\n    v-bind=\"forwarded\"\n    :class=\"\n      cn(\n        'focus-visible:ring-ring inline-flex shrink-0 items-center justify-center transition-colors focus-visible:ring-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-30',\n        !isRight && 'absolute top-1/2 left-0 -translate-y-1/2',\n        !isRight && uiContext.size.value === 'small' && 'p-1.5',\n        !isRight && uiContext.size.value === 'middle' && 'p-3',\n        !isRight && uiContext.size.value === 'large' && 'p-4',\n        isRight && 'hover:bg-accent col-start-2 row-start-2 h-full w-auto rounded-none border-t border-l p-0 px-2',\n        props.class,\n      )\n    \"\n  >\n    <slot>\n      <Minus :class=\"iconSize\" aria-hidden=\"true\" />\n    </slot>\n  </NumberFieldDecrement>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/number-field/NumberFieldDecrement.vue"
    },
    {
      "path": "packages/registry-vue/components/number-field/NumberFieldIncrement.vue",
      "content": "<script setup lang=\"ts\">\nimport type { NumberFieldIncrementProps } from 'reka-ui'\nimport type { HTMLAttributes } from 'vue'\nimport { computed } from 'vue'\nimport { reactiveOmit } from '@vueuse/core'\nimport { Plus } from 'lucide-vue-next'\nimport { NumberFieldIncrement, useForwardProps } from 'reka-ui'\nimport { cn } from '@/lib/utils'\nimport { injectNumberFieldContext } from './NumberFieldContext'\n\nconst props = defineProps<NumberFieldIncrementProps & { class?: HTMLAttributes['class'] }>()\n\nconst delegatedProps = reactiveOmit(props, 'class')\n\nconst forwarded = useForwardProps(delegatedProps)\n\nconst uiContext = injectNumberFieldContext()\n\nconst isRight = computed(() => uiContext.controlsPosition.value === 'right')\n\nconst iconSize = computed(() => {\n  switch (uiContext.size.value) {\n    case 'small':\n      return 'h-3 w-3'\n    case 'large':\n      return 'h-5 w-5'\n    default:\n      return 'h-4 w-4'\n  }\n})\n</script>\n\n<template>\n  <NumberFieldIncrement\n    data-uipkge\n    data-slot=\"increment\"\n    v-bind=\"forwarded\"\n    :class=\"\n      cn(\n        'focus-visible:ring-ring inline-flex shrink-0 items-center justify-center transition-colors focus-visible:ring-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-30',\n        !isRight && 'absolute top-1/2 right-0 -translate-y-1/2',\n        !isRight && uiContext.size.value === 'small' && 'p-1.5',\n        !isRight && uiContext.size.value === 'middle' && 'p-3',\n        !isRight && uiContext.size.value === 'large' && 'p-4',\n        isRight && 'hover:bg-accent col-start-2 row-start-1 h-full w-auto rounded-none border-l p-0 px-2',\n        props.class,\n      )\n    \"\n  >\n    <slot>\n      <Plus :class=\"iconSize\" aria-hidden=\"true\" />\n    </slot>\n  </NumberFieldIncrement>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/number-field/NumberFieldIncrement.vue"
    },
    {
      "path": "packages/registry-vue/components/number-field/NumberFieldInput.vue",
      "content": "<script setup lang=\"ts\">\nimport type { HTMLAttributes } from 'vue'\nimport { computed, nextTick, onMounted, ref, watch } from 'vue'\nimport { injectNumberFieldRootContext } from 'reka-ui'\nimport { cn } from '@/lib/utils'\nimport { injectNumberFieldContext } from './NumberFieldContext'\n\nconst props = defineProps<{\n  class?: HTMLAttributes['class']\n}>()\n\nconst rootContext = injectNumberFieldRootContext()\nconst uiContext = injectNumberFieldContext()\n\nconst inputRef = ref<HTMLInputElement>()\n\nonMounted(() => {\n  if (inputRef.value) {\n    rootContext.onInputElement(inputRef.value)\n  }\n})\n\nconst displayValue = ref('')\nconst isUserTyping = ref(false)\n\nfunction updateDisplayValue(val: number | undefined) {\n  const formatter = uiContext.formatter.value\n  if (formatter) {\n    displayValue.value = formatter(val)\n  } else {\n    displayValue.value = rootContext.textValue.value\n  }\n}\n\nwatch(\n  () => rootContext.modelValue.value,\n  (val) => {\n    if (!isUserTyping.value) {\n      updateDisplayValue(val)\n    }\n  },\n  { immediate: true },\n)\n\nwatch(\n  () => rootContext.textValue.value,\n  (val) => {\n    if (!isUserTyping.value && !uiContext.formatter.value) {\n      displayValue.value = val\n    }\n  },\n)\n\nfunction handleFocus() {\n  isUserTyping.value = true\n}\n\nfunction handleInput(event: Event) {\n  displayValue.value = (event.target as HTMLInputElement).value\n}\n\nfunction commitValue() {\n  isUserTyping.value = false\n  const raw = displayValue.value.trim()\n\n  if (raw === '') {\n    rootContext.modelValue.value = undefined\n  } else {\n    const parser = uiContext.parser.value\n    let num: number | undefined\n    if (parser) {\n      num = parser(raw)\n    } else {\n      num = Number(raw)\n    }\n\n    if (num !== undefined && !Number.isNaN(num)) {\n      rootContext.applyInputValue(String(num))\n    }\n  }\n\n  nextTick(() => {\n    updateDisplayValue(rootContext.modelValue.value)\n  })\n}\n\nfunction handleBlur() {\n  commitValue()\n}\n\nfunction handleKeydown(event: KeyboardEvent) {\n  if (event.key === 'ArrowUp') {\n    if (uiContext.keyboard.value) {\n      event.preventDefault()\n      rootContext.handleIncrease()\n    }\n  } else if (event.key === 'ArrowDown') {\n    if (uiContext.keyboard.value) {\n      event.preventDefault()\n      rootContext.handleDecrease()\n    }\n  } else if (event.key === 'PageUp') {\n    if (uiContext.keyboard.value) {\n      event.preventDefault()\n      rootContext.handleIncrease(10)\n    }\n  } else if (event.key === 'PageDown') {\n    if (uiContext.keyboard.value) {\n      event.preventDefault()\n      rootContext.handleDecrease(10)\n    }\n  } else if (event.key === 'Home') {\n    if (uiContext.keyboard.value) {\n      event.preventDefault()\n      rootContext.handleMinMaxValue('min')\n    }\n  } else if (event.key === 'End') {\n    if (uiContext.keyboard.value) {\n      event.preventDefault()\n      rootContext.handleMinMaxValue('max')\n    }\n  } else if (event.key === 'Enter') {\n    commitValue()\n  }\n}\n\nfunction handleWheel(event: WheelEvent) {\n  if (rootContext.disableWheelChange.value) return\n  if (event.target !== document.activeElement) return\n  if (Math.abs(event.deltaY) <= Math.abs(event.deltaX)) return\n  event.preventDefault()\n  if (event.deltaY > 0) {\n    rootContext.invertWheelChange.value ? rootContext.handleDecrease() : rootContext.handleIncrease()\n  } else {\n    rootContext.invertWheelChange.value ? rootContext.handleIncrease() : rootContext.handleDecrease()\n  }\n}\n\nconst isRight = computed(() => uiContext.controlsPosition.value === 'right')\n\nconst sizeClasses = computed(() => {\n  switch (uiContext.size.value) {\n    case 'small':\n      return 'h-7 text-xs px-2 py-0.5'\n    case 'large':\n      return 'h-11 text-base px-4 py-2'\n    default:\n      return 'h-9 text-sm px-3 py-1'\n  }\n})\n\nconst statusClasses = computed(() => {\n  switch (uiContext.status.value) {\n    case 'error':\n      return 'border-destructive focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 aria-invalid:border-destructive'\n    case 'warning':\n      return 'border-[var(--warning)] focus-visible:ring-[var(--warning)]/20'\n    default:\n      return ''\n  }\n})\n</script>\n\n<template>\n  <div data-uipkge data-slot=\"input\" :class=\"cn('relative flex-1', isRight && 'col-span-1 row-span-2', props.class)\">\n    <span\n      v-if=\"uiContext.prefix.value\"\n      class=\"text-muted-foreground pointer-events-none absolute top-1/2 left-2 -translate-y-1/2 text-sm\"\n    >\n      {{ uiContext.prefix.value }}\n    </span>\n    <input\n      ref=\"inputRef\"\n      v-model=\"displayValue\"\n      type=\"text\"\n      role=\"spinbutton\"\n      :aria-valuenow=\"rootContext.modelValue.value\"\n      :aria-valuemin=\"rootContext.min.value\"\n      :aria-valuemax=\"rootContext.max.value\"\n      :inputmode=\"rootContext.inputMode.value\"\n      :disabled=\"rootContext.disabled.value\"\n      :readonly=\"rootContext.readonly.value\"\n      :aria-invalid=\"uiContext.status.value === 'error' ? true : undefined\"\n      autocomplete=\"off\"\n      autocorrect=\"off\"\n      spellcheck=\"false\"\n      aria-roledescription=\"Number field\"\n      @focus=\"handleFocus\"\n      @input=\"handleInput\"\n      @blur=\"handleBlur\"\n      @keydown=\"handleKeydown\"\n      @wheel=\"handleWheel\"\n      :class=\"\n        cn(\n          'placeholder:text-muted-foreground w-full bg-transparent text-center shadow-sm transition-colors outline-none disabled:cursor-not-allowed disabled:opacity-50',\n          !isRight && 'border-input focus-visible:ring-ring rounded-md border focus-visible:ring-1',\n          isRight && 'rounded-none border-0 focus-visible:ring-0',\n          uiContext.prefix.value && 'pl-6',\n          uiContext.suffix.value && 'pr-6',\n          sizeClasses,\n          !isRight && statusClasses,\n        )\n      \"\n    />\n    <span\n      v-if=\"uiContext.suffix.value\"\n      class=\"text-muted-foreground pointer-events-none absolute top-1/2 right-2 -translate-y-1/2 text-sm\"\n    >\n      {{ uiContext.suffix.value }}\n    </span>\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/number-field/NumberFieldInput.vue"
    },
    {
      "path": "packages/registry-vue/components/number-field/index.ts",
      "content": "export { default as NumberField } from './NumberField.vue'\nexport { default as NumberFieldContent } from './NumberFieldContent.vue'\nexport { default as NumberFieldDecrement } from './NumberFieldDecrement.vue'\nexport { default as NumberFieldIncrement } from './NumberFieldIncrement.vue'\nexport { default as NumberFieldInput } from './NumberFieldInput.vue'\nexport { injectNumberFieldContext, provideNumberFieldContext } from './NumberFieldContext'\nexport type { NumberFieldControlsPosition, NumberFieldSize, NumberFieldStatus } from './NumberField.vue'\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/number-field/index.ts"
    }
  ],
  "dependencies": [
    "@vueuse/core",
    "lucide-vue-next",
    "reka-ui"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Numeric input with stepper buttons, min/max bounds, step size, and decimal precision. Use for quantities, prices, and any field that should be a number rather than free text.",
  "categories": [
    "form"
  ]
}