{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "mass-input",
  "title": "Mass Input",
  "description": "The nx-ui Mass Input block.",
  "registryDependencies": [
    "@nx-ui/data-material-terms",
    "@nx-ui/native-select",
    "@nx-ui/number-input",
    "@nx-ui/utils"
  ],
  "files": [
    {
      "path": "components/blocks/domain/mass-input/index.ts",
      "content": "export {\n  MassInput,\n  type MassInputProps,\n  type MassUnit,\n} from \"./mass-input\"\n",
      "type": "registry:component",
      "target": "components/blocks/domain/mass-input/index.ts"
    },
    {
      "path": "components/blocks/domain/mass-input/mass-input.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport {\n  findClass,\n  findTerm,\n  getReferenceDensity,\n} from \"@/lib/data/material-terms\"\nimport { cn } from \"@/lib/utils\"\nimport { NativeSelect, NativeSelectOption } from \"@/components/ui/native-select\"\nimport { NumberInput } from \"@/components/ui/number-input\"\n\nexport type MassUnit = \"t\" | \"kg\" | \"m3\"\n\nexport interface MassInputProps {\n  /** Controlled quantity in the current unit. */\n  value?: number | null\n  /** Initial quantity for uncontrolled usage. */\n  defaultValue?: number\n  onValueChange?: (value: number | null) => void\n  /** Controlled unit — tonnes, kilograms or cubic metres. */\n  unit?: MassUnit\n  /** Initial unit for uncontrolled usage. Defaults to `\"t\"`. */\n  defaultUnit?: MassUnit\n  onUnitChange?: (unit: MassUnit) => void\n  /**\n   * A material term code (e.g. `\"TOPSOIL_BS3882\"`) or leaf class key\n   * (e.g. `\"soil.topsoil\"`) — the mid reference density drives the live\n   * weight ↔ volume conversion beneath the control.\n   */\n  material?: string\n  /** Explicit density in t/m³. Takes precedence over `material`. */\n  density?: number\n  size?: \"default\" | \"sm\"\n  disabled?: boolean\n  /** Marks the control as invalid (destructive border + ring). */\n  invalid?: boolean\n  placeholder?: string\n  id?: string\n  name?: string\n  className?: string\n}\n\nconst UNITS: { value: MassUnit; label: string }[] = [\n  { value: \"t\", label: \"t\" },\n  { value: \"kg\", label: \"kg\" },\n  { value: \"m3\", label: \"m³\" },\n]\n\n/** Resolve a density (t/m³) from an explicit override or a material lookup. */\nfunction resolveDensity(\n  material?: string,\n  density?: number\n): number | undefined {\n  if (typeof density === \"number\" && density > 0) return density\n  if (!material) return undefined\n  const term = findTerm(material)\n  if (term) return getReferenceDensity(term.parent)?.mid\n  const cls = findClass(material)\n  if (cls) return getReferenceDensity(cls.key)?.mid\n  return undefined\n}\n\n/** Normalise a quantity in any supported unit to tonnes. */\nfunction toTonnes(value: number, unit: MassUnit, density: number): number {\n  if (unit === \"kg\") return value / 1000\n  if (unit === \"m3\") return value * density\n  return value\n}\n\nconst tonnesFormat = new Intl.NumberFormat(\"en-GB\", {\n  maximumFractionDigits: 2,\n})\nconst kilogramsFormat = new Intl.NumberFormat(\"en-GB\", {\n  maximumFractionDigits: 0,\n})\nconst cubicMetresFormat = new Intl.NumberFormat(\"en-GB\", {\n  minimumFractionDigits: 1,\n  maximumFractionDigits: 1,\n})\n\n/** The converted equivalents of `value` (in `unit`) for every other unit. */\nfunction equivalents(\n  value: number,\n  unit: MassUnit,\n  density: number\n): string[] {\n  const tonnes = toTonnes(value, unit, density)\n  const parts: string[] = []\n  if (unit !== \"t\") parts.push(`${tonnesFormat.format(tonnes)} t`)\n  if (unit !== \"kg\")\n    parts.push(`${kilogramsFormat.format(Math.round(tonnes * 1000))} kg`)\n  if (unit !== \"m3\")\n    parts.push(`${cubicMetresFormat.format(tonnes / density)} m³`)\n  return parts\n}\n\n/**\n * MassInput — a weight/volume quantity control for waste movements: a\n * NumberInput with a unit selector (t / kg / m³) and, when a material (or an\n * explicit density) is known, a live converted equivalent announced via an\n * `aria-live` region — e.g. 12 t of crushed concrete reads \"≈ 8.0 m³\".\n */\nexport function MassInput({\n  value,\n  defaultValue,\n  onValueChange,\n  unit: unitProp,\n  defaultUnit = \"t\",\n  onUnitChange,\n  material,\n  density: densityProp,\n  size = \"default\",\n  disabled,\n  invalid,\n  placeholder,\n  id,\n  name,\n  className,\n}: MassInputProps) {\n  const [internalValue, setInternalValue] = React.useState<number | null>(\n    defaultValue ?? null\n  )\n  const [internalUnit, setInternalUnit] = React.useState<MassUnit>(defaultUnit)\n\n  const currentValue = value !== undefined ? value : internalValue\n  const currentUnit = unitProp ?? internalUnit\n  const density = resolveDensity(material, densityProp)\n\n  function handleValueChange(next: number | null) {\n    if (value === undefined) setInternalValue(next)\n    onValueChange?.(next)\n  }\n\n  function handleUnitChange(next: MassUnit) {\n    if (unitProp === undefined) setInternalUnit(next)\n    onUnitChange?.(next)\n  }\n\n  const parts =\n    currentValue != null && density ? equivalents(currentValue, currentUnit, density) : []\n\n  return (\n    <div className={cn(\"flex flex-col gap-1.5\", className)}>\n      <div className=\"flex items-stretch gap-2\">\n        <NumberInput\n          className=\"flex-1\"\n          size={size}\n          value={currentValue}\n          onValueChange={handleValueChange}\n          min={0}\n          disabled={disabled}\n          invalid={invalid}\n          placeholder={placeholder ?? \"0\"}\n          id={id}\n          name={name}\n          aria-label=\"Quantity\"\n        />\n        <NativeSelect\n          value={currentUnit}\n          onChange={(e) => handleUnitChange(e.target.value as MassUnit)}\n          size={size}\n          disabled={disabled}\n          aria-label=\"Unit\"\n          className=\"w-24\"\n        >\n          {UNITS.map((u) => (\n            <NativeSelectOption key={u.value} value={u.value}>\n              {u.label}\n            </NativeSelectOption>\n          ))}\n        </NativeSelect>\n      </div>\n      <p aria-live=\"polite\" className=\"min-h-5 text-sm text-text-tertiary\">\n        {parts.length > 0 ? (\n          <>\n            <span aria-hidden=\"true\">≈ </span>\n            <span className=\"tabular-nums\">{parts.join(\" · \")}</span>\n            {density ? (\n              <span className=\"text-text-quaternary\">\n                {\" \"}\n                at {density} t/m³\n              </span>\n            ) : null}\n          </>\n        ) : null}\n      </p>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/blocks/domain/mass-input/mass-input.tsx"
    }
  ],
  "type": "registry:block"
}