{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "quantity-input",
  "title": "Quantity Input",
  "description": "A number input paired with a unit select, for entering material quantities with conversion between units.",
  "registryDependencies": [
    "@nx-ui/utils",
    "@nx-ui/number-input",
    "@nx-ui/select"
  ],
  "files": [
    {
      "path": "components/ui/quantity-input.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { NumberInput } from \"@/components/ui/number-input\"\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\"\n\n/** A selectable unit, with an optional conversion factor back to a base unit. */\nexport type QuantityUnit = {\n  /** Stable key used as the select value, e.g. \"t\". */\n  value: string\n  /** Display label, e.g. \"tonnes\". */\n  label: string\n  /** Multiplier to convert an amount in this unit into the base unit (the first entry). */\n  toBase?: number\n}\n\n/**\n * Curated unit sets for Nexus ReGen material quantities. `toBase` factors are\n * relative to the first entry in each set (tonnes / cubic metres).\n */\nexport const MASS_UNITS: QuantityUnit[] = [\n  { value: \"t\", label: \"tonnes\", toBase: 1 },\n  { value: \"kg\", label: \"kg\", toBase: 0.001 },\n]\n\nexport const VOLUME_UNITS: QuantityUnit[] = [\n  { value: \"m3\", label: \"m³\", toBase: 1 },\n  { value: \"L\", label: \"litres\", toBase: 0.001 },\n]\n\nexport type QuantityInputProps = {\n  /** Quantity amount, in the currently selected unit (controlled). */\n  value?: number | null\n  /** Uncontrolled initial amount. */\n  defaultValue?: number | null\n  /** Fires with the amount whenever the number field changes. */\n  onValueChange?: (value: number | null) => void\n  /** Currently selected unit value (controlled). */\n  unit?: string\n  /** Uncontrolled initial unit value. */\n  defaultUnit?: string\n  /** Fires with the new unit key, and the equivalent amount converted via `toBase`, when the unit changes. */\n  onUnitChange?: (unit: string, convertedValue: number | null) => void\n  /** Selectable units. Defaults to `MASS_UNITS` (tonnes / kg). */\n  units?: QuantityUnit[]\n  min?: number\n  max?: number\n  step?: number\n  disabled?: boolean\n  invalid?: boolean\n  placeholder?: string\n  className?: string\n  id?: string\n  name?: string\n  /** Accessible name for the amount field when no visible `<label>` is associated. */\n  \"aria-label\"?: string\n}\n\n/**\n * QuantityInput — a domain-flavoured amount-with-unit control: a\n * `NumberInput` for the amount, plus a unit `Select` (tonnes/kg, m³/litres,\n * or a custom `units` list). When `toBase` conversion factors are supplied,\n * switching units re-scales the displayed amount so the underlying quantity\n * stays constant.\n */\nfunction QuantityInput({\n  value,\n  defaultValue = null,\n  onValueChange,\n  unit,\n  defaultUnit,\n  onUnitChange,\n  units = MASS_UNITS,\n  min,\n  max,\n  step = 1,\n  disabled,\n  invalid,\n  placeholder,\n  className,\n  id,\n  name,\n  \"aria-label\": ariaLabel,\n}: QuantityInputProps) {\n  const isAmountControlled = value !== undefined\n  const isUnitControlled = unit !== undefined\n\n  const [internalAmount, setInternalAmount] = React.useState<number | null>(\n    defaultValue\n  )\n  const [internalUnit, setInternalUnit] = React.useState<string>(\n    defaultUnit ?? units[0]?.value ?? \"\"\n  )\n\n  const amount = isAmountControlled ? value : internalAmount\n  const selectedUnit = isUnitControlled ? unit : internalUnit\n\n  function handleAmountChange(next: number | null) {\n    if (!isAmountControlled) setInternalAmount(next)\n    onValueChange?.(next)\n  }\n\n  function handleUnitChange(nextUnit: string | null) {\n    if (!nextUnit) return\n    const from = units.find((u) => u.value === selectedUnit)\n    const to = units.find((u) => u.value === nextUnit)\n\n    let converted = amount ?? null\n    if (converted !== null && from?.toBase && to?.toBase) {\n      converted = (converted * from.toBase) / to.toBase\n    }\n\n    if (!isUnitControlled) setInternalUnit(nextUnit)\n    if (!isAmountControlled) setInternalAmount(converted)\n    onUnitChange?.(nextUnit, converted)\n  }\n\n  return (\n    <div\n      data-slot=\"quantity-input\"\n      className={cn(\n        \"flex w-full min-w-0 items-stretch gap-2 data-[disabled]:pointer-events-none data-[disabled]:opacity-50\",\n        className\n      )}\n      data-disabled={disabled ? true : undefined}\n    >\n      <NumberInput\n        id={id}\n        name={name}\n        aria-label={ariaLabel}\n        value={amount ?? undefined}\n        onValueChange={(next) => handleAmountChange(next)}\n        min={min}\n        max={max}\n        step={step}\n        disabled={disabled}\n        invalid={invalid}\n        placeholder={placeholder}\n        className=\"flex-1\"\n      />\n      <Select\n        value={selectedUnit}\n        onValueChange={handleUnitChange}\n        disabled={disabled}\n      >\n        <SelectTrigger aria-label=\"Unit\" className=\"w-24 shrink-0\">\n          <SelectValue />\n        </SelectTrigger>\n        <SelectContent>\n          {units.map((u) => (\n            <SelectItem key={u.value} value={u.value}>\n              {u.label}\n            </SelectItem>\n          ))}\n        </SelectContent>\n      </Select>\n    </div>\n  )\n}\n\nexport { QuantityInput }\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}