{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "haulage-vehicle-select",
  "title": "Haulage Vehicle Select",
  "description": "A picker for common UK construction and waste haulage vehicles — tippers, grabs, artics, RoRo, skips and more — with drawn side-profile artwork, payload/volume/CO2e details and loads-required and movement-emissions estimates, in grid, list and compact form variants.",
  "dependencies": [
    "@untitledui/icons@^0.0.22"
  ],
  "registryDependencies": [
    "@nx-ui/badge",
    "@nx-ui/data-haulage-vehicles",
    "@nx-ui/popover",
    "@nx-ui/utils"
  ],
  "files": [
    {
      "path": "components/blocks/domain/haulage-vehicle-select/haulage-vehicle-select.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  Check,\n  ChevronDown,\n  Cloud01,\n  Container,\n  Feather,\n  Package,\n  Scale01,\n  SearchLg,\n  Tool01,\n  Truck01,\n  XClose,\n} from \"@untitledui/icons\"\n\nimport { cn } from \"@/lib/utils\"\nimport {\n  HAULAGE_VEHICLES,\n  loadsRequired,\n  movementCo2eKg,\n  type HaulageVehicle,\n  type HaulageVehicleCategory,\n} from \"@/lib/data/haulage-vehicles\"\nimport { Badge } from \"@/components/ui/badge\"\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/components/ui/popover\"\n\nimport { VehicleArtwork } from \"./vehicle-artwork\"\nimport type {\n  HaulageQuantity,\n  HaulageVehicleSelectProps,\n} from \"./types\"\n\n// ── Category metadata ─────────────────────────────────────────────────────────\n\nconst CATEGORY_META: Record<\n  HaulageVehicleCategory,\n  {\n    label: string\n    Icon: React.ComponentType<{ className?: string }>\n    badge: \"brand\" | \"blue\" | \"success\" | \"warning\"\n  }\n> = {\n  \"rigid-tipper\": { label: \"Rigid tipper\", Icon: Truck01, badge: \"brand\" },\n  articulated: { label: \"Articulated\", Icon: Truck01, badge: \"blue\" },\n  container: { label: \"Container\", Icon: Container, badge: \"success\" },\n  specialist: { label: \"Specialist\", Icon: Tool01, badge: \"warning\" },\n}\n\nconst CATEGORY_ORDER: HaulageVehicleCategory[] = [\n  \"rigid-tipper\",\n  \"articulated\",\n  \"container\",\n  \"specialist\",\n]\n\n// ── Small formatting helpers ──────────────────────────────────────────────────\n\n/** Round to at most one decimal place and drop a trailing \".0\". */\nfunction num(n: number): string {\n  return Number.isInteger(n) ? String(n) : n.toFixed(1)\n}\n\n/**\n * Format the per-tonne-km carbon-intensity figure to 2 decimal places.\n *\n * Values across the fleet cluster tightly (0.08-0.18 kg/t·km), so the 1dp\n * rounding used by `num()` collapses almost every vehicle to the same\n * \"0.1\" label. This keeps the differentiation the chip exists to show.\n */\nfunction co2ePerTonneKm(n: number): string {\n  return n.toFixed(2)\n}\n\n/** A tonnage total, scaled to tonnes/kt with sensible precision. */\nfunction tonnesLabel(t: number): string {\n  if (t >= 1000) return `${num(t / 1000)} kt`\n  if (t >= 100) return `${Math.round(t)} t`\n  return `${num(t)} t`\n}\n\n/** kgCO2e total, promoted to tonnes above 1 000 kg. */\nfunction co2eLabel(kg: number): string {\n  if (kg >= 1000) return `${num(kg / 1000)} t CO2e`\n  return `${Math.round(kg)} kg CO2e`\n}\n\nfunction hasQuantity(q?: HaulageQuantity): q is HaulageQuantity {\n  return Boolean(q && ((q.tonnes && q.tonnes > 0) || (q.m3 && q.m3 > 0)))\n}\n\n// ── Controlled/uncontrolled selection hook ────────────────────────────────────\n\nfunction useSelection(\n  value: string | null | undefined,\n  defaultValue: string | null | undefined,\n  onValueChange: HaulageVehicleSelectProps[\"onValueChange\"],\n  vehicles: HaulageVehicle[]\n) {\n  const isControlled = value !== undefined\n  const [internal, setInternal] = React.useState<string | null>(\n    defaultValue ?? null\n  )\n  const selectedKey = isControlled ? (value ?? null) : internal\n\n  const select = React.useCallback(\n    (key: string | null) => {\n      if (!isControlled) setInternal(key)\n      const vehicle = key ? vehicles.find((v) => v.key === key) ?? null : null\n      onValueChange?.(key, vehicle)\n    },\n    [isControlled, onValueChange, vehicles]\n  )\n\n  return { selectedKey, select }\n}\n\n// ── Metric chips shared across variants ───────────────────────────────────────\n\n/** The core spec line: payload · body · GVW. */\nfunction SpecRow({ vehicle }: { vehicle: HaulageVehicle }) {\n  return (\n    <dl className=\"flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-text-tertiary tabular-nums\">\n      <div className=\"flex items-center gap-1\">\n        <Scale01 className=\"size-3.5 text-fg-quaternary\" aria-hidden=\"true\" />\n        <dt className=\"sr-only\">Payload</dt>\n        <dd>\n          <span className=\"font-medium text-text-secondary\">\n            {num(vehicle.payloadT)} t\n          </span>{\" \"}\n          payload\n        </dd>\n      </div>\n      <div className=\"flex items-center gap-1\">\n        <Package className=\"size-3.5 text-fg-quaternary\" aria-hidden=\"true\" />\n        <dt className=\"sr-only\">Body volume</dt>\n        <dd>\n          <span className=\"font-medium text-text-secondary\">\n            {num(vehicle.bodyVolumeM3)} m³\n          </span>\n        </dd>\n      </div>\n      <div className=\"flex items-center gap-1\">\n        <dt className=\"sr-only\">Gross vehicle weight</dt>\n        <dd>\n          {num(vehicle.grossVehicleWeightT)} t <span className=\"text-fg-quaternary\">GVW</span>\n        </dd>\n      </div>\n    </dl>\n  )\n}\n\n/**\n * The DEFRA-style carbon-intensity chip (kgCO2e per tonne-km).\n *\n * This chip is always rendered inside a `role=\"radio\"` card button (grid and\n * list variants), so it can't wrap itself in a focusable `Tooltip` trigger\n * without nesting an interactive element inside the radio and breaking that\n * semantics. Instead the explanation ships as an `sr-only` span — reachable by\n * screen readers, invisible to sighted users who already read the visible\n * abbreviated label — rather than a native `title` attribute, which is\n * unreachable by keyboard/touch and most screen readers.\n */\nfunction Co2eChip({ vehicle }: { vehicle: HaulageVehicle }) {\n  return (\n    <span className=\"inline-flex items-center gap-1 rounded-full border border-utility-green-200 bg-utility-green-50 px-2 py-0.5 text-xs font-medium text-utility-green-700 tabular-nums\">\n      <Cloud01 className=\"size-3\" aria-hidden=\"true\" />\n      <span aria-hidden=\"true\">{co2ePerTonneKm(vehicle.co2ePerTonneKmKg)} kg/t·km</span>\n      <span className=\"sr-only\">\n        {co2ePerTonneKm(vehicle.co2ePerTonneKmKg)} kg CO2e per tonne-kilometre, indicative\n        DEFRA-style figure including empty running\n      </span>\n    </span>\n  )\n}\n\n/** Self-loading / road-legal capability badges. */\nfunction CapabilityBadges({ vehicle }: { vehicle: HaulageVehicle }) {\n  return (\n    <div className=\"flex flex-wrap items-center gap-1.5\">\n      {vehicle.selfLoading ? (\n        <Badge variant=\"blue\" className=\"gap-1\">\n          <Package className=\"size-3\" aria-hidden=\"true\" />\n          Self-loading\n        </Badge>\n      ) : null}\n      {vehicle.roadLegal ? (\n        <Badge variant=\"gray\">Road-legal</Badge>\n      ) : (\n        <Badge variant=\"warning\">Site-only</Badge>\n      )}\n    </div>\n  )\n}\n\n/**\n * The estimator context block: loads required for the given quantity and, when a\n * distance is supplied, the movement's total CO2e. Rendered only when a quantity\n * is set.\n */\nfunction EstimateContext({\n  vehicle,\n  quantity,\n  distanceKm,\n  emphasis = false,\n}: {\n  vehicle: HaulageVehicle\n  quantity: HaulageQuantity\n  distanceKm?: number\n  emphasis?: boolean\n}) {\n  const loads = loadsRequired(vehicle, {\n    tonnes: quantity.tonnes,\n    m3: quantity.m3,\n  })\n  const totalCo2e =\n    distanceKm && distanceKm > 0\n      ? movementCo2eKg(vehicle, distanceKm, { loads })\n      : null\n\n  return (\n    <div className=\"flex flex-wrap items-baseline gap-x-3 gap-y-1\">\n      <span\n        className={cn(\n          \"font-semibold text-foreground tabular-nums\",\n          emphasis ? \"text-lg\" : \"text-sm\"\n        )}\n      >\n        ≈ {loads} load{loads === 1 ? \"\" : \"s\"}\n      </span>\n      {totalCo2e != null ? (\n        <span className=\"text-sm font-medium text-utility-green-700 tabular-nums\">\n          ≈ {co2eLabel(totalCo2e)} total\n        </span>\n      ) : null}\n    </div>\n  )\n}\n\n/** Full dimensions + access notes + typical use — the details view. */\nfunction VehicleDetails({ vehicle }: { vehicle: HaulageVehicle }) {\n  return (\n    <div className=\"flex flex-col gap-3\">\n      <div className=\"flex items-center gap-3\">\n        <div className=\"w-28 shrink-0 text-text-tertiary\">\n          <VehicleArtwork vehicle={vehicle} />\n        </div>\n        <div className=\"min-w-0\">\n          <div className=\"text-sm font-semibold text-foreground\">\n            {vehicle.name}\n          </div>\n          {vehicle.aka?.length ? (\n            <div className=\"truncate text-xs text-text-tertiary\">\n              {vehicle.aka.join(\" · \")}\n            </div>\n          ) : null}\n        </div>\n      </div>\n      <p className=\"text-sm text-text-secondary\">{vehicle.typicalUse}</p>\n      <dl className=\"grid grid-cols-2 gap-x-4 gap-y-2 text-xs tabular-nums\">\n        <Detail label=\"Payload\" value={`${num(vehicle.payloadT)} t`} />\n        <Detail\n          label=\"Body volume\"\n          value={`${num(vehicle.bodyVolumeM3)} m³${vehicle.bodyVolumeNote ? ` · ${vehicle.bodyVolumeNote}` : \"\"}`}\n        />\n        <Detail label=\"GVW / MAM\" value={`${num(vehicle.grossVehicleWeightT)} t`} />\n        <Detail label=\"Axles\" value={String(vehicle.axles)} />\n        <Detail\n          label=\"Dimensions (L·W·H)\"\n          value={`${num(vehicle.lengthM)} · ${num(vehicle.widthM)} · ${num(vehicle.heightM)} m`}\n        />\n        <Detail\n          label=\"Carbon\"\n          value={`${co2ePerTonneKm(vehicle.co2ePerTonneKmKg)} kg/t·km`}\n        />\n        {vehicle.euroClass ? (\n          <Detail label=\"Emissions\" value={vehicle.euroClass} />\n        ) : null}\n      </dl>\n      {vehicle.accessNotes ? (\n        <p className=\"rounded-lg bg-bg-secondary px-3 py-2 text-xs text-text-tertiary\">\n          <span className=\"font-medium text-text-secondary\">Access: </span>\n          {vehicle.accessNotes}\n        </p>\n      ) : null}\n    </div>\n  )\n}\n\nfunction Detail({ label, value }: { label: string; value: string }) {\n  return (\n    <div className=\"min-w-0\">\n      <dt className=\"text-fg-quaternary\">{label}</dt>\n      <dd className=\"truncate font-medium text-text-secondary\">{value}</dd>\n    </div>\n  )\n}\n\n// ── Filtering ─────────────────────────────────────────────────────────────────\n\ninterface FilterState {\n  categories: Set<HaulageVehicleCategory>\n  roadLegalOnly: boolean\n  selfLoadingOnly: boolean\n}\n\nfunction useFilteredVehicles(\n  vehicles: HaulageVehicle[],\n  filters: FilterState\n) {\n  return React.useMemo(() => {\n    return vehicles.filter((v) => {\n      if (filters.categories.size > 0 && !filters.categories.has(v.category)) {\n        return false\n      }\n      if (filters.roadLegalOnly && !v.roadLegal) return false\n      if (filters.selfLoadingOnly && !v.selfLoading) return false\n      return true\n    })\n  }, [vehicles, filters])\n}\n\n/** Category chips + road-legal / self-loading toggles. */\nfunction FilterBar({\n  vehicles,\n  filters,\n  setFilters,\n}: {\n  vehicles: HaulageVehicle[]\n  filters: FilterState\n  setFilters: React.Dispatch<React.SetStateAction<FilterState>>\n}) {\n  const available = React.useMemo(() => {\n    const set = new Set<HaulageVehicleCategory>()\n    for (const v of vehicles) set.add(v.category)\n    return CATEGORY_ORDER.filter((c) => set.has(c))\n  }, [vehicles])\n\n  const toggleCategory = (c: HaulageVehicleCategory) => {\n    setFilters((prev) => {\n      const next = new Set(prev.categories)\n      if (next.has(c)) next.delete(c)\n      else next.add(c)\n      return { ...prev, categories: next }\n    })\n  }\n\n  return (\n    <div className=\"flex flex-wrap items-center gap-2\">\n      <div role=\"group\" aria-label=\"Filter by category\" className=\"flex flex-wrap gap-1.5\">\n        {available.map((c) => {\n          const meta = CATEGORY_META[c]\n          const active = filters.categories.has(c)\n          return (\n            <button\n              key={c}\n              type=\"button\"\n              aria-pressed={active}\n              onClick={() => toggleCategory(c)}\n              className={cn(\n                \"inline-flex cursor-pointer items-center gap-1.5 rounded-full border px-3 py-1 text-xs font-medium transition-colors outline-none focus-visible:ring-4 focus-visible:ring-ring/25\",\n                active\n                  ? \"border-primary/40 bg-primary/10 text-foreground\"\n                  : \"border-border bg-background text-text-tertiary hover:bg-muted hover:text-foreground\"\n              )}\n            >\n              <meta.Icon className=\"size-3.5\" aria-hidden=\"true\" />\n              {meta.label}\n            </button>\n          )\n        })}\n      </div>\n      <span className=\"mx-0.5 h-4 w-px bg-border\" aria-hidden=\"true\" />\n      <FilterToggle\n        label=\"Road-legal\"\n        active={filters.roadLegalOnly}\n        onToggle={() =>\n          setFilters((p) => ({ ...p, roadLegalOnly: !p.roadLegalOnly }))\n        }\n      />\n      <FilterToggle\n        label=\"Self-loading\"\n        active={filters.selfLoadingOnly}\n        onToggle={() =>\n          setFilters((p) => ({ ...p, selfLoadingOnly: !p.selfLoadingOnly }))\n        }\n      />\n    </div>\n  )\n}\n\nfunction FilterToggle({\n  label,\n  active,\n  onToggle,\n}: {\n  label: string\n  active: boolean\n  onToggle: () => void\n}) {\n  return (\n    <button\n      type=\"button\"\n      aria-pressed={active}\n      onClick={onToggle}\n      className={cn(\n        \"inline-flex cursor-pointer items-center gap-1.5 rounded-full border px-3 py-1 text-xs font-medium transition-colors outline-none focus-visible:ring-4 focus-visible:ring-ring/25\",\n        active\n          ? \"border-utility-blue-200 bg-utility-blue-50 text-utility-blue-700\"\n          : \"border-border bg-background text-text-tertiary hover:bg-muted hover:text-foreground\"\n      )}\n    >\n      {active ? (\n        <Check className=\"size-3.5\" aria-hidden=\"true\" />\n      ) : (\n        <span className=\"size-3.5 rounded-full border border-border\" aria-hidden=\"true\" />\n      )}\n      {label}\n    </button>\n  )\n}\n\nfunction EmptyState({ onReset }: { onReset: () => void }) {\n  return (\n    <div className=\"flex flex-col items-center gap-2 rounded-xl border border-dashed border-border bg-bg-secondary/40 px-6 py-10 text-center\">\n      <Feather className=\"size-6 text-fg-quaternary\" aria-hidden=\"true\" />\n      <p className=\"text-sm font-medium text-foreground\">No vehicles match</p>\n      <p className=\"text-sm text-text-tertiary\">\n        No vehicle in the fleet matches every active filter.\n      </p>\n      <button\n        type=\"button\"\n        onClick={onReset}\n        className=\"mt-1 cursor-pointer text-sm font-semibold text-text-brand-secondary hover:underline\"\n      >\n        Clear filters\n      </button>\n    </div>\n  )\n}\n\n// ── Grid variant (radiogroup of cards) ────────────────────────────────────────\n\nfunction GridCard({\n  vehicle,\n  selected,\n  disabled,\n  tabIndex,\n  quantity,\n  distanceKm,\n  onSelect,\n  registerRef,\n  onKeyDown,\n}: {\n  vehicle: HaulageVehicle\n  selected: boolean\n  disabled: boolean\n  tabIndex: number\n  quantity?: HaulageQuantity\n  distanceKm?: number\n  onSelect: () => void\n  registerRef: (el: HTMLButtonElement | null) => void\n  onKeyDown: (e: React.KeyboardEvent<HTMLButtonElement>) => void\n}) {\n  const meta = CATEGORY_META[vehicle.category]\n  return (\n    <button\n      ref={registerRef}\n      type=\"button\"\n      role=\"radio\"\n      aria-checked={selected}\n      disabled={disabled}\n      tabIndex={disabled ? -1 : tabIndex}\n      onClick={disabled ? undefined : onSelect}\n      onKeyDown={onKeyDown}\n      className={cn(\n        \"group/vehicle-card relative flex cursor-pointer flex-col gap-3 rounded-xl border bg-background p-4 text-left shadow-xs transition-all duration-200 outline-none\",\n        \"hover:-translate-y-0.5 hover:shadow-md\",\n        \"focus-visible:border-ring focus-visible:ring-4 focus-visible:ring-ring/25\",\n        selected\n          ? \"border-primary/50 bg-primary/5 ring-1 ring-primary/30 dark:bg-primary/10\"\n          : \"border-border hover:border-border-primary\",\n        disabled &&\n          \"pointer-events-none cursor-not-allowed opacity-50 hover:translate-y-0 hover:shadow-xs\"\n      )}\n    >\n      <span\n        className={cn(\n          \"absolute top-3 right-3 flex size-5 items-center justify-center rounded-full border transition-colors\",\n          selected\n            ? \"border-primary bg-primary text-primary-foreground\"\n            : \"border-input bg-background\"\n        )}\n        aria-hidden=\"true\"\n      >\n        {selected ? <Check className=\"size-3.5\" /> : null}\n      </span>\n\n      <div className=\"text-text-tertiary transition-colors group-hover/vehicle-card:text-text-secondary\">\n        <VehicleArtwork vehicle={vehicle} className=\"max-h-20\" />\n      </div>\n\n      <div className=\"flex flex-col gap-2\">\n        <div className=\"flex items-start justify-between gap-2\">\n          <div className=\"min-w-0\">\n            <div className=\"text-sm font-semibold text-foreground\">\n              {vehicle.name}\n            </div>\n            {vehicle.aka?.length ? (\n              <div className=\"truncate text-xs text-text-tertiary\">\n                {vehicle.aka[0]}\n              </div>\n            ) : null}\n          </div>\n          <Badge variant={meta.badge} className=\"shrink-0\">\n            {meta.label}\n          </Badge>\n        </div>\n\n        <SpecRow vehicle={vehicle} />\n\n        <div className=\"flex flex-wrap items-center gap-1.5\">\n          <Co2eChip vehicle={vehicle} />\n          <CapabilityBadges vehicle={vehicle} />\n        </div>\n\n        {hasQuantity(quantity) ? (\n          <div className=\"mt-1 border-t border-border pt-2.5\">\n            <EstimateContext\n              vehicle={vehicle}\n              quantity={quantity}\n              distanceKm={distanceKm}\n              emphasis\n            />\n          </div>\n        ) : null}\n      </div>\n    </button>\n  )\n}\n\nfunction GridVariant({\n  vehicles,\n  selectedKey,\n  disabledKeys,\n  quantity,\n  distanceKm,\n  onSelect,\n}: {\n  vehicles: HaulageVehicle[]\n  selectedKey: string | null\n  disabledKeys: Set<string>\n  quantity?: HaulageQuantity\n  distanceKm?: number\n  onSelect: (key: string) => void\n}) {\n  const refs = React.useRef<Map<string, HTMLButtonElement>>(new Map())\n  const groupRef = React.useRef<HTMLDivElement>(null)\n  // Tracks whether focus is currently inside this radiogroup, so the recovery\n  // effect below only steals focus when the group actually had it — never\n  // grabs focus unprompted.\n  const hasFocusWithinRef = React.useRef(false)\n\n  const enabled = vehicles.filter((v) => !disabledKeys.has(v.key))\n\n  // Roving tabindex: the selected card, or the first enabled one, is tabbable.\n  const rovingKey =\n    selectedKey && enabled.some((v) => v.key === selectedKey)\n      ? selectedKey\n      : enabled[0]?.key ?? null\n\n  const focusKey = React.useCallback((key: string) => {\n    refs.current.get(key)?.focus()\n  }, [])\n\n  // Roving-tabindex recovery: if the card holding focus is removed by a filter\n  // chip or a disabledKeys change, the browser drops focus to <body>. When that\n  // happens while this group held focus, move focus onto the new roving card\n  // instead of leaving it stranded.\n  React.useEffect(() => {\n    if (!hasFocusWithinRef.current) return\n    if (!rovingKey) return\n    const group = groupRef.current\n    if (!group || group.contains(document.activeElement)) return\n    refs.current.get(rovingKey)?.focus()\n  }, [rovingKey])\n\n  const handleKeyDown = React.useCallback(\n    (e: React.KeyboardEvent<HTMLButtonElement>, key: string) => {\n      const keys = enabled.map((v) => v.key)\n      const idx = keys.indexOf(key)\n      if (idx === -1) return\n\n      let nextIdx = idx\n      switch (e.key) {\n        case \"ArrowRight\":\n        case \"ArrowDown\":\n          nextIdx = (idx + 1) % keys.length\n          break\n        case \"ArrowLeft\":\n        case \"ArrowUp\":\n          nextIdx = (idx - 1 + keys.length) % keys.length\n          break\n        case \"Home\":\n          nextIdx = 0\n          break\n        case \"End\":\n          nextIdx = keys.length - 1\n          break\n        case \" \":\n        case \"Enter\":\n          e.preventDefault()\n          onSelect(key)\n          return\n        default:\n          return\n      }\n      e.preventDefault()\n      const nextKey = keys[nextIdx]\n      focusKey(nextKey)\n      // Arrow navigation moves selection, matching native radiogroup behaviour.\n      onSelect(nextKey)\n    },\n    [enabled, focusKey, onSelect]\n  )\n\n  return (\n    <div\n      ref={groupRef}\n      role=\"radiogroup\"\n      aria-label=\"Choose a haulage vehicle\"\n      className=\"grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3\"\n      onFocus={() => {\n        hasFocusWithinRef.current = true\n      }}\n      onBlur={(e) => {\n        // A real focus move to another element (inside or outside the group)\n        // sets relatedTarget; leave hasFocusWithinRef alone unless focus is\n        // actually leaving the group. relatedTarget is null when the focused\n        // card itself was removed from the DOM (filter/disabledKeys change),\n        // which the recovery effect below needs to still see as \"had focus\".\n        if (e.relatedTarget && e.currentTarget.contains(e.relatedTarget as Node)) {\n          return\n        }\n        hasFocusWithinRef.current = e.relatedTarget == null\n      }}\n    >\n      {vehicles.map((vehicle) => {\n        const disabled = disabledKeys.has(vehicle.key)\n        return (\n          <GridCard\n            key={vehicle.key}\n            vehicle={vehicle}\n            selected={selectedKey === vehicle.key}\n            disabled={disabled}\n            tabIndex={vehicle.key === rovingKey ? 0 : -1}\n            quantity={quantity}\n            distanceKm={distanceKm}\n            onSelect={() => onSelect(vehicle.key)}\n            registerRef={(el) => {\n              if (el) refs.current.set(vehicle.key, el)\n              else refs.current.delete(vehicle.key)\n            }}\n            onKeyDown={(e) => handleKeyDown(e, vehicle.key)}\n          />\n        )\n      })}\n    </div>\n  )\n}\n\n// ── List variant (dense rows + details popover) ───────────────────────────────\n\nfunction ListRow({\n  vehicle,\n  selected,\n  disabled,\n  tabIndex,\n  quantity,\n  distanceKm,\n  onSelect,\n  registerRef,\n  onKeyDown,\n}: {\n  vehicle: HaulageVehicle\n  selected: boolean\n  disabled: boolean\n  tabIndex: number\n  quantity?: HaulageQuantity\n  distanceKm?: number\n  onSelect: () => void\n  registerRef: (el: HTMLButtonElement | null) => void\n  onKeyDown: (e: React.KeyboardEvent<HTMLButtonElement>) => void\n}) {\n  const meta = CATEGORY_META[vehicle.category]\n  return (\n    <div\n      className={cn(\n        \"flex items-center gap-3 rounded-xl border bg-background p-3 shadow-xs transition-colors\",\n        selected\n          ? \"border-primary/50 bg-primary/5 ring-1 ring-primary/30 dark:bg-primary/10\"\n          : \"border-border\",\n        disabled && \"opacity-50\"\n      )}\n    >\n      <button\n        ref={registerRef}\n        type=\"button\"\n        role=\"radio\"\n        aria-checked={selected}\n        disabled={disabled}\n        tabIndex={disabled ? -1 : tabIndex}\n        onClick={disabled ? undefined : onSelect}\n        onKeyDown={onKeyDown}\n        className={cn(\n          \"flex min-w-0 flex-1 cursor-pointer items-center gap-3 rounded-lg text-left outline-none focus-visible:ring-4 focus-visible:ring-ring/25\",\n          disabled && \"pointer-events-none cursor-not-allowed\"\n        )}\n      >\n        <span\n          className={cn(\n            \"flex size-5 shrink-0 items-center justify-center rounded-full border transition-colors\",\n            selected\n              ? \"border-primary bg-primary text-primary-foreground\"\n              : \"border-input bg-background\"\n          )}\n          aria-hidden=\"true\"\n        >\n          {selected ? <Check className=\"size-3.5\" /> : null}\n        </span>\n        <div className=\"w-24 shrink-0 text-text-tertiary sm:w-28\">\n          <VehicleArtwork vehicle={vehicle} className=\"max-h-14\" />\n        </div>\n        <div className=\"flex min-w-0 flex-1 flex-col gap-1.5\">\n          <div className=\"flex flex-wrap items-center gap-x-2 gap-y-1\">\n            <span className=\"text-sm font-semibold text-foreground\">\n              {vehicle.name}\n            </span>\n            <Badge variant={meta.badge}>{meta.label}</Badge>\n            {vehicle.selfLoading ? (\n              <Badge variant=\"blue\">Self-loading</Badge>\n            ) : null}\n            {!vehicle.roadLegal ? (\n              <Badge variant=\"warning\">Site-only</Badge>\n            ) : null}\n          </div>\n          <SpecRow vehicle={vehicle} />\n        </div>\n        <div className=\"hidden shrink-0 flex-col items-end gap-1 sm:flex\">\n          <Co2eChip vehicle={vehicle} />\n          {hasQuantity(quantity) ? (\n            <EstimateContext\n              vehicle={vehicle}\n              quantity={quantity}\n              distanceKm={distanceKm}\n            />\n          ) : null}\n        </div>\n      </button>\n      <Popover>\n        <PopoverTrigger\n          render={\n            <button\n              type=\"button\"\n              aria-label={`Details for ${vehicle.name}`}\n              className=\"shrink-0 cursor-pointer rounded-lg border border-border bg-background px-2.5 py-1.5 text-xs font-medium text-text-tertiary shadow-xs transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-4 focus-visible:ring-ring/25 outline-none\"\n            >\n              Details\n            </button>\n          }\n        />\n        <PopoverContent align=\"end\" className=\"w-80\">\n          <VehicleDetails vehicle={vehicle} />\n        </PopoverContent>\n      </Popover>\n    </div>\n  )\n}\n\nfunction ListVariant({\n  vehicles,\n  selectedKey,\n  disabledKeys,\n  quantity,\n  distanceKm,\n  onSelect,\n}: {\n  vehicles: HaulageVehicle[]\n  selectedKey: string | null\n  disabledKeys: Set<string>\n  quantity?: HaulageQuantity\n  distanceKm?: number\n  onSelect: (key: string) => void\n}) {\n  const refs = React.useRef<Map<string, HTMLButtonElement>>(new Map())\n  const groupRef = React.useRef<HTMLDivElement>(null)\n  // Tracks whether focus is currently inside this radiogroup, so the recovery\n  // effect below only steals focus when the group actually had it — never\n  // grabs focus unprompted.\n  const hasFocusWithinRef = React.useRef(false)\n  const enabled = vehicles.filter((v) => !disabledKeys.has(v.key))\n  const rovingKey =\n    selectedKey && enabled.some((v) => v.key === selectedKey)\n      ? selectedKey\n      : enabled[0]?.key ?? null\n\n  // Roving-tabindex recovery: if the row holding focus is removed by a filter\n  // chip or a disabledKeys change, the browser drops focus to <body>. When that\n  // happens while this group held focus, move focus onto the new roving row\n  // instead of leaving it stranded.\n  React.useEffect(() => {\n    if (!hasFocusWithinRef.current) return\n    if (!rovingKey) return\n    const group = groupRef.current\n    if (!group || group.contains(document.activeElement)) return\n    refs.current.get(rovingKey)?.focus()\n  }, [rovingKey])\n\n  const handleKeyDown = React.useCallback(\n    (e: React.KeyboardEvent<HTMLButtonElement>, key: string) => {\n      const keys = enabled.map((v) => v.key)\n      const idx = keys.indexOf(key)\n      if (idx === -1) return\n      let nextIdx = idx\n      switch (e.key) {\n        case \"ArrowDown\":\n          nextIdx = (idx + 1) % keys.length\n          break\n        case \"ArrowUp\":\n          nextIdx = (idx - 1 + keys.length) % keys.length\n          break\n        case \"Home\":\n          nextIdx = 0\n          break\n        case \"End\":\n          nextIdx = keys.length - 1\n          break\n        case \" \":\n        case \"Enter\":\n          e.preventDefault()\n          onSelect(key)\n          return\n        default:\n          return\n      }\n      e.preventDefault()\n      const nextKey = keys[nextIdx]\n      refs.current.get(nextKey)?.focus()\n      onSelect(nextKey)\n    },\n    [enabled, onSelect]\n  )\n\n  return (\n    <div\n      ref={groupRef}\n      role=\"radiogroup\"\n      aria-label=\"Choose a haulage vehicle\"\n      className=\"flex flex-col gap-2\"\n      onFocus={() => {\n        hasFocusWithinRef.current = true\n      }}\n      onBlur={(e) => {\n        // A real focus move to another element (inside or outside the group)\n        // sets relatedTarget; leave hasFocusWithinRef alone unless focus is\n        // actually leaving the group. relatedTarget is null when the focused\n        // row itself was removed from the DOM (filter/disabledKeys change),\n        // which the recovery effect below needs to still see as \"had focus\".\n        if (e.relatedTarget && e.currentTarget.contains(e.relatedTarget as Node)) {\n          return\n        }\n        hasFocusWithinRef.current = e.relatedTarget == null\n      }}\n    >\n      {vehicles.map((vehicle) => {\n        const disabled = disabledKeys.has(vehicle.key)\n        return (\n          <ListRow\n            key={vehicle.key}\n            vehicle={vehicle}\n            selected={selectedKey === vehicle.key}\n            disabled={disabled}\n            tabIndex={vehicle.key === rovingKey ? 0 : -1}\n            quantity={quantity}\n            distanceKm={distanceKm}\n            onSelect={() => onSelect(vehicle.key)}\n            registerRef={(el) => {\n              if (el) refs.current.set(vehicle.key, el)\n              else refs.current.delete(vehicle.key)\n            }}\n            onKeyDown={(e) => handleKeyDown(e, vehicle.key)}\n          />\n        )\n      })}\n    </div>\n  )\n}\n\n// ── Compact variant (combobox-style trigger + popover listbox) ────────────────\n\nfunction CompactVariant({\n  vehicles,\n  selectedKey,\n  disabledKeys,\n  quantity,\n  distanceKm,\n  onSelect,\n  className,\n}: {\n  vehicles: HaulageVehicle[]\n  selectedKey: string | null\n  disabledKeys: Set<string>\n  quantity?: HaulageQuantity\n  distanceKm?: number\n  onSelect: (key: string | null) => void\n  className?: string\n}) {\n  const [open, setOpen] = React.useState(false)\n  const [query, setQuery] = React.useState(\"\")\n  const selected = selectedKey\n    ? vehicles.find((v) => v.key === selectedKey) ?? null\n    : null\n\n  const filtered = React.useMemo(() => {\n    const q = query.trim().toLowerCase()\n    if (!q) return vehicles\n    return vehicles.filter((v) => {\n      const haystack = [v.name, ...(v.aka ?? []), CATEGORY_META[v.category].label]\n        .join(\" \")\n        .toLowerCase()\n      return haystack.includes(q)\n    })\n  }, [vehicles, query])\n\n  const choose = (key: string) => {\n    onSelect(key)\n    setOpen(false)\n    setQuery(\"\")\n  }\n\n  return (\n    <Popover\n      open={open}\n      onOpenChange={(next) => {\n        setOpen(next)\n        if (!next) setQuery(\"\")\n      }}\n    >\n      <PopoverTrigger\n        render={\n          <button\n            type=\"button\"\n            aria-label=\"Select a haulage vehicle\"\n            className={cn(\n              \"flex h-10 w-full min-w-64 cursor-pointer items-center justify-between gap-2 rounded-lg border border-input bg-background px-3 text-sm shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-4 focus-visible:ring-ring/25\",\n              className\n            )}\n          >\n            {selected ? (\n              <span className=\"flex min-w-0 items-center gap-2\">\n                <span className=\"w-10 shrink-0 text-text-tertiary\">\n                  <VehicleArtwork vehicle={selected} className=\"max-h-6\" />\n                </span>\n                <span className=\"truncate font-medium text-foreground\">\n                  {selected.name}\n                </span>\n              </span>\n            ) : (\n              <span className=\"text-muted-foreground\">Select a vehicle…</span>\n            )}\n            <ChevronDown className=\"size-4 shrink-0 text-fg-quaternary\" aria-hidden=\"true\" />\n          </button>\n        }\n      />\n      <PopoverContent\n        align=\"start\"\n        className=\"w-(--anchor-width) min-w-72 gap-0 p-0\"\n      >\n        <div className=\"flex items-center gap-2 border-b border-border px-3 py-2\">\n          <SearchLg className=\"size-4 shrink-0 text-fg-quaternary\" aria-hidden=\"true\" />\n          <input\n            type=\"text\"\n            autoFocus\n            value={query}\n            onChange={(e) => setQuery(e.target.value)}\n            placeholder=\"Search vehicles…\"\n            aria-label=\"Search vehicles\"\n            className=\"w-full bg-transparent text-sm text-foreground outline-none placeholder:text-muted-foreground\"\n          />\n          {selected ? (\n            <button\n              type=\"button\"\n              onClick={() => onSelect(null)}\n              aria-label=\"Clear selection\"\n              className=\"shrink-0 cursor-pointer rounded-md p-0.5 text-fg-quaternary hover:text-foreground focus-visible:ring-4 focus-visible:ring-ring/25 outline-none\"\n            >\n              <XClose className=\"size-4\" />\n            </button>\n          ) : null}\n        </div>\n        <div role=\"listbox\" aria-label=\"Vehicles\" className=\"max-h-72 overflow-y-auto p-1\">\n          {filtered.length === 0 ? (\n            <p className=\"px-3 py-6 text-center text-sm text-text-tertiary\">\n              No vehicles match “{query}”.\n            </p>\n          ) : (\n            filtered.map((vehicle) => {\n              const disabled = disabledKeys.has(vehicle.key)\n              const isSelected = selectedKey === vehicle.key\n              return (\n                <button\n                  key={vehicle.key}\n                  type=\"button\"\n                  role=\"option\"\n                  aria-selected={isSelected}\n                  disabled={disabled}\n                  onClick={disabled ? undefined : () => choose(vehicle.key)}\n                  className={cn(\n                    \"flex w-full cursor-pointer items-center gap-2.5 rounded-lg px-2 py-1.5 text-left outline-none transition-colors\",\n                    \"hover:bg-muted focus-visible:bg-muted focus-visible:ring-2 focus-visible:ring-ring/25\",\n                    isSelected && \"bg-primary/5 dark:bg-primary/10\",\n                    disabled && \"pointer-events-none cursor-not-allowed opacity-50\"\n                  )}\n                >\n                  <span className=\"w-12 shrink-0 text-text-tertiary\">\n                    <VehicleArtwork vehicle={vehicle} className=\"max-h-7\" />\n                  </span>\n                  <span className=\"flex min-w-0 flex-1 flex-col\">\n                    <span className=\"truncate text-sm font-medium text-foreground\">\n                      {vehicle.name}\n                    </span>\n                    <span className=\"truncate text-xs text-text-tertiary tabular-nums\">\n                      {num(vehicle.payloadT)} t · {num(vehicle.bodyVolumeM3)} m³\n                      {hasQuantity(quantity)\n                        ? ` · ≈ ${loadsRequired(vehicle, { tonnes: quantity.tonnes, m3: quantity.m3 })} loads`\n                        : \"\"}\n                      {hasQuantity(quantity) && distanceKm && distanceKm > 0\n                        ? ` · ≈ ${co2eLabel(movementCo2eKg(vehicle, distanceKm, { loads: loadsRequired(vehicle, { tonnes: quantity.tonnes, m3: quantity.m3 }) }))}`\n                        : \"\"}\n                    </span>\n                  </span>\n                  {isSelected ? (\n                    <Check className=\"size-4 shrink-0 text-primary\" aria-hidden=\"true\" />\n                  ) : null}\n                </button>\n              )\n            })\n          )}\n        </div>\n      </PopoverContent>\n    </Popover>\n  )\n}\n\n// ── Public component ──────────────────────────────────────────────────────────\n\n/**\n * HaulageVehicleSelect — pick a UK haulage vehicle for a construction-material,\n * muck-away or waste movement.\n *\n * Three variants share one selection model: a card **grid** (radiogroup with\n * roving focus and arrow-key selection), detailed **list** rows with a details\n * popover, and a **compact** combobox trigger for forms. Supply a `quantity`\n * (and optionally `distanceKm`) to surface the loads required and the movement's\n * indicative total CO2e on every card — the estimator story. Filter chips and\n * capability toggles narrow the fleet on the grid and list variants.\n */\nexport function HaulageVehicleSelect({\n  vehicles = HAULAGE_VEHICLES,\n  value,\n  defaultValue,\n  onValueChange,\n  variant = \"grid\",\n  quantity,\n  distanceKm,\n  filterable,\n  disabledKeys,\n  className,\n}: HaulageVehicleSelectProps) {\n  const { selectedKey, select } = useSelection(\n    value,\n    defaultValue,\n    onValueChange,\n    vehicles\n  )\n\n  const disabledSet = React.useMemo(\n    () => new Set(disabledKeys ?? []),\n    [disabledKeys]\n  )\n\n  const showFilters = filterable ?? variant !== \"compact\"\n\n  const [filters, setFilters] = React.useState<FilterState>({\n    categories: new Set(),\n    roadLegalOnly: false,\n    selfLoadingOnly: false,\n  })\n\n  const visible = useFilteredVehicles(\n    vehicles,\n    showFilters\n      ? filters\n      : { categories: new Set(), roadLegalOnly: false, selfLoadingOnly: false }\n  )\n\n  const resetFilters = React.useCallback(\n    () =>\n      setFilters({\n        categories: new Set(),\n        roadLegalOnly: false,\n        selfLoadingOnly: false,\n      }),\n    []\n  )\n\n  if (variant === \"compact\") {\n    return (\n      <CompactVariant\n        vehicles={vehicles}\n        selectedKey={selectedKey}\n        disabledKeys={disabledSet}\n        quantity={quantity}\n        distanceKm={distanceKm}\n        onSelect={select}\n        className={className}\n      />\n    )\n  }\n\n  return (\n    <div className={cn(\"flex flex-col gap-4\", className)}>\n      {showFilters ? (\n        <FilterBar\n          vehicles={vehicles}\n          filters={filters}\n          setFilters={setFilters}\n        />\n      ) : null}\n\n      {visible.length === 0 ? (\n        <EmptyState onReset={resetFilters} />\n      ) : variant === \"list\" ? (\n        <ListVariant\n          vehicles={visible}\n          selectedKey={selectedKey}\n          disabledKeys={disabledSet}\n          quantity={quantity}\n          distanceKm={distanceKm}\n          onSelect={select}\n        />\n      ) : (\n        <GridVariant\n          vehicles={visible}\n          selectedKey={selectedKey}\n          disabledKeys={disabledSet}\n          quantity={quantity}\n          distanceKm={distanceKm}\n          onSelect={select}\n        />\n      )}\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/blocks/domain/haulage-vehicle-select/haulage-vehicle-select.tsx"
    },
    {
      "path": "components/blocks/domain/haulage-vehicle-select/index.ts",
      "content": "export { HaulageVehicleSelect } from \"./haulage-vehicle-select\"\nexport { VehicleArtwork, type VehicleArtworkProps } from \"./vehicle-artwork\"\nexport type {\n  HaulageQuantity,\n  HaulageVehicleSelectProps,\n  HaulageVehicleSelectVariant,\n} from \"./types\"\n",
      "type": "registry:component",
      "target": "components/blocks/domain/haulage-vehicle-select/index.ts"
    },
    {
      "path": "components/blocks/domain/haulage-vehicle-select/types.ts",
      "content": "/** Shared types for the HaulageVehicleSelect block. */\n\nimport type { HaulageVehicle } from \"@/lib/data/haulage-vehicles\"\n\n/** A material quantity to size loads against — tonnage and/or a volume. */\nexport interface HaulageQuantity {\n  tonnes?: number\n  m3?: number\n}\n\n/** Layout of the selector. */\nexport type HaulageVehicleSelectVariant = \"grid\" | \"list\" | \"compact\"\n\nexport interface HaulageVehicleSelectProps {\n  /** Vehicles to offer; defaults to the full `HAULAGE_VEHICLES` dataset. */\n  vehicles?: HaulageVehicle[]\n  /** Controlled selected vehicle key, or `null` for none. */\n  value?: string | null\n  /** Uncontrolled initial selection. */\n  defaultValue?: string | null\n  /** Fires with the new key and the resolved vehicle (both `null` when cleared). */\n  onValueChange?: (key: string | null, vehicle: HaulageVehicle | null) => void\n  /**\n   * \"grid\" — card grid with radiogroup semantics (default);\n   * \"list\" — dense detailed rows with a details popover;\n   * \"compact\" — a Select-style trigger + combobox popover, for forms.\n   */\n  variant?: HaulageVehicleSelectVariant\n  /**\n   * When set, each card/row shows the whole loads required for this quantity and\n   * (with `distanceKm`) the estimated total CO2e for the movement.\n   */\n  quantity?: HaulageQuantity\n  /** With `quantity`, the one-way haul distance in km used for the CO2e estimate. */\n  distanceKm?: number\n  /**\n   * Show category filter chips and road-legal / self-loading toggles. Defaults to\n   * true on the grid and list variants, off on compact.\n   */\n  filterable?: boolean\n  /** Vehicle keys rendered non-selectable. */\n  disabledKeys?: string[]\n  className?: string\n}\n",
      "type": "registry:component",
      "target": "components/blocks/domain/haulage-vehicle-select/types.ts"
    },
    {
      "path": "components/blocks/domain/haulage-vehicle-select/vehicle-artwork.tsx",
      "content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport type { HaulageVehicle } from \"@/lib/data/haulage-vehicles\"\n\n/**\n * VehicleArtwork — inline-SVG side-profile silhouettes for every haulage vehicle\n * key. Themable: strokes use `currentColor` and fills use semantic token classes\n * (`fill-muted`, `fill-card`, …) so the artwork follows light/dark and every\n * brand. No raster images.\n *\n * Shared drawing system — every profile uses the same coordinate conventions so\n * the fleet reads at a consistent scale and stroke:\n *   - viewBox height 100, ground line at y=82, cab/body baseline ~y=74.\n *   - Wheels are `Wheel` (tyre + hub + centre) at a common radius (r≈9), so a\n *     4-wheeler and a 44-tonne artic share the same \"size of a wheel\" hint.\n *   - 1-metre tick marks along the ground give a real scale reference; widths of\n *     the viewBox scale with the vehicle's true length.\n *   - A small human figure stands at the left for human scale.\n * Strokes are 2 units; caps/joins rounded; `vectorEffect=\"non-scaling-stroke\"`\n * keeps line weight even when the SVG is sized down via className.\n */\n\nconst GROUND_Y = 82\nconst WHEEL_R = 8.5\nconst STROKE = 2\n\n/** A wheel: outer tyre, inner hub ring, centre. Draws from its ground-contact x. */\nfunction Wheel({ cx }: { cx: number }) {\n  const cy = GROUND_Y - WHEEL_R\n  return (\n    <g className=\"text-foreground\">\n      <circle\n        cx={cx}\n        cy={cy}\n        r={WHEEL_R}\n        className=\"fill-muted\"\n        stroke=\"currentColor\"\n        strokeWidth={STROKE}\n        vectorEffect=\"non-scaling-stroke\"\n      />\n      <circle\n        cx={cx}\n        cy={cy}\n        r={WHEEL_R * 0.45}\n        className=\"fill-card\"\n        stroke=\"currentColor\"\n        strokeWidth={STROKE * 0.75}\n        vectorEffect=\"non-scaling-stroke\"\n      />\n      <circle cx={cx} cy={cy} r={WHEEL_R * 0.12} className=\"fill-foreground\" />\n    </g>\n  )\n}\n\n/** The shared ground line plus evenly spaced 1 m tick marks across the width. */\nfunction Ground({ width, metres }: { width: number; metres: number }) {\n  const startX = 6\n  const endX = width - 6\n  const step = (endX - startX) / metres\n  const ticks = Array.from({ length: metres + 1 }, (_, i) => startX + i * step)\n  return (\n    <g className=\"text-border\">\n      <line\n        x1={2}\n        y1={GROUND_Y}\n        x2={width - 2}\n        y2={GROUND_Y}\n        stroke=\"currentColor\"\n        strokeWidth={STROKE}\n        strokeLinecap=\"round\"\n        vectorEffect=\"non-scaling-stroke\"\n      />\n      {ticks.map((x, i) => (\n        <line\n          key={i}\n          x1={x}\n          y1={GROUND_Y}\n          x2={x}\n          y2={GROUND_Y + 3}\n          stroke=\"currentColor\"\n          strokeWidth={STROKE * 0.75}\n          strokeLinecap=\"round\"\n          vectorEffect=\"non-scaling-stroke\"\n        />\n      ))}\n    </g>\n  )\n}\n\n/** A small human figure for scale, standing on the ground at x. ~1.8 m tall. */\nfunction HumanScale({ x }: { x: number }) {\n  const feet = GROUND_Y\n  const head = feet - 20\n  return (\n    <g\n      className=\"text-fg-quaternary\"\n      stroke=\"currentColor\"\n      strokeWidth={STROKE * 0.75}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      fill=\"none\"\n      vectorEffect=\"non-scaling-stroke\"\n      aria-hidden=\"true\"\n    >\n      <circle cx={x} cy={head + 2.5} r={2.5} className=\"fill-fg-quaternary\" stroke=\"none\" />\n      <line x1={x} y1={head + 5} x2={x} y2={feet - 7} vectorEffect=\"non-scaling-stroke\" />\n      <line x1={x} y1={head + 8} x2={x - 3.5} y2={head + 13} vectorEffect=\"non-scaling-stroke\" />\n      <line x1={x} y1={head + 8} x2={x + 3.5} y2={head + 13} vectorEffect=\"non-scaling-stroke\" />\n      <line x1={x} y1={feet - 7} x2={x - 3} y2={feet} vectorEffect=\"non-scaling-stroke\" />\n      <line x1={x} y1={feet - 7} x2={x + 3} y2={feet} vectorEffect=\"non-scaling-stroke\" />\n    </g>\n  )\n}\n\n/** Common stroke props for all body/panel paths. */\nconst line = {\n  stroke: \"currentColor\",\n  strokeWidth: STROKE,\n  strokeLinecap: \"round\" as const,\n  strokeLinejoin: \"round\" as const,\n  vectorEffect: \"non-scaling-stroke\" as const,\n}\n\n/** A rigid tractor-unit cab (bonneted/flat-front lorry cab) ending at `rearX`. */\nfunction Cab({\n  frontX,\n  rearX,\n  roofY,\n}: {\n  frontX: number\n  rearX: number\n  roofY: number\n}) {\n  const base = GROUND_Y - 5\n  const windscreenX = frontX + (rearX - frontX) * 0.5\n  return (\n    <g className=\"text-foreground\">\n      <path\n        d={`M ${frontX} ${base}\n            L ${frontX} ${roofY + 6}\n            Q ${frontX} ${roofY} ${frontX + 5} ${roofY}\n            L ${rearX} ${roofY}\n            L ${rearX} ${base} Z`}\n        className=\"fill-card\"\n        {...line}\n      />\n      {/* windscreen + door split */}\n      <path\n        d={`M ${frontX + 4} ${roofY + 4} L ${windscreenX} ${roofY + 4} L ${windscreenX} ${roofY + 14} L ${frontX + 4} ${roofY + 14} Z`}\n        className=\"fill-muted\"\n        {...line}\n        strokeWidth={STROKE * 0.75}\n      />\n      <line x1={windscreenX + 4} y1={roofY + 3} x2={windscreenX + 4} y2={base} {...line} strokeWidth={STROKE * 0.75} />\n    </g>\n  )\n}\n\n// ── Individual vehicle profiles ──────────────────────────────────────────────\n// Each returns just the vehicle body/wheels; Ground + HumanScale + the <svg>\n// wrapper are added by VehicleArtwork. All share the y-conventions above.\n\nfunction TipperRigid({\n  wheelXs,\n  cabFrontX,\n  cabRearX,\n  bodyRearX,\n}: {\n  wheelXs: number[]\n  cabFrontX: number\n  cabRearX: number\n  bodyRearX: number\n}) {\n  // Muck-away tipper body: a tall headboard rising just behind the cab, the\n  // floor raking gently down to a lower tailgate at the rear — the recognisable\n  // rigid-tipper silhouette. The body sits directly on the chassis rail.\n  const bodyFrontX = cabRearX\n  const chassisY = GROUND_Y - 20\n  const bodyTopFront = chassisY - 26 // headboard (tall, behind the cab)\n  const bodyTopRear = chassisY - 18 // tailgate (lower)\n  return (\n    <g className=\"text-foreground\">\n      <Cab frontX={cabFrontX} rearX={cabRearX} roofY={GROUND_Y - 30} />\n      {/* chassis rail */}\n      <line x1={cabRearX - 2} y1={chassisY} x2={bodyRearX + 3} y2={chassisY} {...line} strokeWidth={STROKE * 0.75} />\n      {/* raked muck body sitting on the chassis */}\n      <path\n        d={`M ${bodyFrontX} ${chassisY}\n            L ${bodyFrontX} ${bodyTopFront}\n            L ${bodyRearX} ${bodyTopRear}\n            L ${bodyRearX} ${chassisY} Z`}\n        className=\"fill-muted\"\n        {...line}\n      />\n      {/* tailgate hinge detail at the rear */}\n      <line x1={bodyRearX} y1={bodyTopRear + 2} x2={bodyRearX + 4} y2={bodyTopRear + 7} {...line} strokeWidth={STROKE * 0.75} />\n      {wheelXs.map((x) => (\n        <Wheel key={x} cx={x} />\n      ))}\n    </g>\n  )\n}\n\nexport interface VehicleArtworkProps {\n  /** The vehicle to draw. Pass this or `vehicleKey`. */\n  vehicle?: HaulageVehicle\n  /** The vehicle key to draw. Pass this or `vehicle`. */\n  vehicleKey?: string\n  /** Sizing / colour overrides — the SVG scales to its box. */\n  className?: string\n}\n\n/** viewBox width per profile (height is always 100). Wider = longer vehicle. */\nconst VIEW_W: Record<string, number> = {\n  \"tipper-4x2\": 150,\n  \"tipper-6x4\": 168,\n  \"tipper-8x4\": 188,\n  \"grab-8x4\": 200,\n  \"artic-tipper\": 240,\n  \"walking-floor\": 244,\n  \"roro-hook\": 200,\n  \"skip-loader\": 150,\n  adt: 210,\n  \"truck-mixer\": 186,\n  \"tractor-trailer\": 216,\n}\n\n/** Ground-tick count (≈ vehicle length in whole metres) per profile. */\nconst METRES: Record<string, number> = {\n  \"tipper-4x2\": 7,\n  \"tipper-6x4\": 8,\n  \"tipper-8x4\": 9,\n  \"grab-8x4\": 10,\n  \"artic-tipper\": 16,\n  \"walking-floor\": 16,\n  \"roro-hook\": 10,\n  \"skip-loader\": 7,\n  adt: 10,\n  \"truck-mixer\": 9,\n  \"tractor-trailer\": 11,\n}\n\n/**\n * Render the side-profile for one vehicle. The big `switch` keeps each profile's\n * geometry self-documenting and independent; shared parts (wheels, ground, cab,\n * human scale) come from the helpers above.\n */\nfunction Profile({ vKey, width }: { vKey: string; width: number }) {\n  switch (vKey) {\n    case \"tipper-4x2\":\n      return (\n        <TipperRigid\n          cabFrontX={18}\n          cabRearX={48}\n          bodyRearX={130}\n          wheelXs={[34, 116]}\n        />\n      )\n    case \"tipper-6x4\":\n      return (\n        <TipperRigid\n          cabFrontX={18}\n          cabRearX={48}\n          bodyRearX={158}\n          wheelXs={[34, 128, 146]}\n        />\n      )\n    case \"tipper-8x4\":\n      return (\n        <TipperRigid\n          cabFrontX={18}\n          cabRearX={50}\n          bodyRearX={178}\n          wheelXs={[30, 48, 148, 166]}\n        />\n      )\n    case \"grab-8x4\": {\n      // 8-wheel rigid: cab, a folded hydraulic grab crane mounted behind the\n      // cab, then a muck body. The crane sits in a gap ahead of the body.\n      const chassisY = GROUND_Y - 20\n      const bodyFrontX = 74\n      const bodyRearX = 188\n      const columnX = 60\n      return (\n        <g className=\"text-foreground\">\n          <Cab frontX={18} rearX={50} roofY={GROUND_Y - 30} />\n          {/* chassis rail */}\n          <line x1={50} y1={chassisY} x2={bodyRearX + 3} y2={chassisY} {...line} strokeWidth={STROKE * 0.75} />\n          {/* muck body (raked headboard → tailgate) */}\n          <path\n            d={`M ${bodyFrontX} ${chassisY}\n                L ${bodyFrontX} ${chassisY - 24}\n                L ${bodyRearX} ${chassisY - 17}\n                L ${bodyRearX} ${chassisY} Z`}\n            className=\"fill-muted\"\n            {...line}\n          />\n          {/* grab crane: pedestal + folded knuckle boom rising behind the cab */}\n          <line x1={columnX} y1={chassisY} x2={columnX} y2={GROUND_Y - 54} {...line} />\n          <path\n            d={`M ${columnX} ${GROUND_Y - 54}\n                L ${columnX + 22} ${GROUND_Y - 48}\n                L ${columnX + 13} ${GROUND_Y - 34}`}\n            className=\"fill-none\"\n            {...line}\n          />\n          {/* folded clamshell grab bucket hanging from the boom tip */}\n          <path\n            d={`M ${columnX + 13} ${GROUND_Y - 34}\n                q -5 7 0 11\n                q 5 -5 10 0\n                q 5 -5 0 -11 Z`}\n            className=\"fill-muted\"\n            {...line}\n            strokeWidth={STROKE * 0.9}\n          />\n          <Wheel cx={30} />\n          <Wheel cx={48} />\n          <Wheel cx={150} />\n          <Wheel cx={168} />\n        </g>\n      )\n    }\n    case \"artic-tipper\": {\n      // Tractor unit (steer axle + close-spaced rear bogie) + tipping semi-trailer\n      // on a 3-axle bogie — a UK 44-tonner: 3 tractor axles + 3 trailer axles.\n      const chassisY = GROUND_Y - 20\n      const bodyTopFront = chassisY - 16\n      const bodyTopRear = chassisY - 26\n      return (\n        <g className=\"text-foreground\">\n          <Cab frontX={16} rearX={46} roofY={GROUND_Y - 32} />\n          {/* fifth-wheel coupling + trailer chassis */}\n          <line x1={46} y1={chassisY} x2={230} y2={chassisY} {...line} strokeWidth={STROKE * 0.75} />\n          <circle cx={66} cy={chassisY} r={2.5} className=\"fill-foreground\" />\n          {/* raked bulk tipper body over the trailer */}\n          <path\n            d={`M ${72} ${chassisY - 2}\n                L ${72} ${bodyTopFront}\n                L ${226} ${bodyTopRear}\n                L ${226} ${chassisY - 2} Z`}\n            className=\"fill-muted\"\n            {...line}\n          />\n          <Wheel cx={30} />\n          <Wheel cx={44} />\n          <Wheel cx={58} />\n          <Wheel cx={180} />\n          <Wheel cx={196} />\n          <Wheel cx={212} />\n        </g>\n      )\n    }\n    case \"walking-floor\": {\n      // Tractor unit (steer axle + close-spaced rear bogie) + long, tall enclosed\n      // box trailer (moving-floor) on a 3-axle bogie — 3 tractor + 3 trailer axles.\n      const chassisY = GROUND_Y - 20\n      const boxTop = chassisY - 34\n      return (\n        <g className=\"text-foreground\">\n          <Cab frontX={16} rearX={46} roofY={GROUND_Y - 32} />\n          <line x1={46} y1={chassisY} x2={234} y2={chassisY} {...line} strokeWidth={STROKE * 0.75} />\n          <circle cx={66} cy={chassisY} r={2.5} className=\"fill-foreground\" />\n          {/* long enclosed box */}\n          <path\n            d={`M ${70} ${chassisY - 2} L ${70} ${boxTop} L ${230} ${boxTop} L ${230} ${chassisY - 2} Z`}\n            className=\"fill-muted\"\n            {...line}\n          />\n          {/* corrugation hints + rear door */}\n          <line x1={116} y1={boxTop + 3} x2={116} y2={chassisY - 4} {...line} strokeWidth={STROKE * 0.5} />\n          <line x1={162} y1={boxTop + 3} x2={162} y2={chassisY - 4} {...line} strokeWidth={STROKE * 0.5} />\n          <line x1={216} y1={boxTop + 3} x2={216} y2={chassisY - 4} {...line} strokeWidth={STROKE * 0.6} />\n          <Wheel cx={30} />\n          <Wheel cx={44} />\n          <Wheel cx={58} />\n          <Wheel cx={182} />\n          <Wheel cx={198} />\n          <Wheel cx={214} />\n        </g>\n      )\n    }\n    case \"roro-hook\": {\n      // Rigid 8-wheel chassis carrying an open roll-on/roll-off container, hook\n      // arm at the rear.\n      const chassisY = GROUND_Y - 20\n      const contTop = chassisY - 28\n      return (\n        <g className=\"text-foreground\">\n          <Cab frontX={18} rearX={50} roofY={GROUND_Y - 30} />\n          <line x1={50} y1={chassisY} x2={192} y2={chassisY} {...line} strokeWidth={STROKE * 0.75} />\n          {/* open RoRo container: slightly tapered, open top */}\n          <path\n            d={`M ${58} ${chassisY - 2} L ${64} ${contTop} L ${182} ${contTop} L ${188} ${chassisY - 2} Z`}\n            className=\"fill-muted\"\n            {...line}\n          />\n          <line x1={100} y1={contTop} x2={100} y2={chassisY - 3} {...line} strokeWidth={STROKE * 0.5} />\n          <line x1={144} y1={contTop} x2={144} y2={chassisY - 3} {...line} strokeWidth={STROKE * 0.5} />\n          {/* hook-loader arm hooking the front of the container */}\n          <path\n            d={`M ${188} ${chassisY} L ${196} ${contTop + 6} L ${182} ${contTop + 2}`}\n            className=\"fill-none\"\n            {...line}\n          />\n          <Wheel cx={34} />\n          <Wheel cx={52} />\n          <Wheel cx={158} />\n          <Wheel cx={176} />\n        </g>\n      )\n    }\n    case \"skip-loader\": {\n      // 2-axle rigid with over-cab hydraulic A-frame arms lifting a builder's\n      // skip that sits on the chassis behind the cab.\n      const chassisY = GROUND_Y - 20\n      const skipFrontX = 60\n      const skipRearX = 132\n      const skipTop = chassisY - 18\n      const armTopY = GROUND_Y - 46\n      return (\n        <g className=\"text-foreground\">\n          <Cab frontX={16} rearX={44} roofY={GROUND_Y - 30} />\n          <line x1={44} y1={chassisY} x2={142} y2={chassisY} {...line} strokeWidth={STROKE * 0.75} />\n          {/* tapered builder's skip (wider at the top) on the chassis */}\n          <path\n            d={`M ${skipFrontX} ${chassisY} L ${skipFrontX - 6} ${skipTop} L ${skipRearX + 6} ${skipTop} L ${skipRearX} ${chassisY} Z`}\n            className=\"fill-muted\"\n            {...line}\n          />\n          {/* over-cab lifting A-frame: two arms up over the cab, cross beam,\n              angling back down to the skip's front lifting lugs */}\n          <path\n            d={`M ${skipFrontX + 4} ${skipTop} L ${46} ${armTopY} L ${skipFrontX + 26} ${armTopY}`}\n            className=\"fill-none\"\n            {...line}\n          />\n          {/* lift chains from the beam down to the skip rim */}\n          <line x1={46} y1={armTopY} x2={skipFrontX + 2} y2={skipTop} {...line} strokeWidth={STROKE * 0.7} />\n          <line x1={skipFrontX + 26} y1={armTopY} x2={skipFrontX + 30} y2={skipTop} {...line} strokeWidth={STROKE * 0.7} />\n          <Wheel cx={30} />\n          <Wheel cx={116} />\n        </g>\n      )\n    }\n    case \"adt\": {\n      // Articulated dump truck: front power unit + articulation waist + raked\n      // dump body over big flotation tyres. Site plant, not a road cab.\n      const bodyY = GROUND_Y - 30\n      return (\n        <g className=\"text-foreground\">\n          {/* power unit / bonnet + operator cab */}\n          <path\n            d={`M ${14} ${GROUND_Y - 8}\n                L ${14} ${GROUND_Y - 24}\n                L ${28} ${GROUND_Y - 24}\n                L ${34} ${GROUND_Y - 42}\n                L ${54} ${GROUND_Y - 42}\n                L ${58} ${GROUND_Y - 24}\n                L ${64} ${GROUND_Y - 22}`}\n            className=\"fill-card\"\n            {...line}\n          />\n          {/* articulation waist joint */}\n          <circle cx={70} cy={GROUND_Y - 20} r={3} className=\"fill-muted\" {...line} strokeWidth={STROKE * 0.75} />\n          {/* raked dump body */}\n          <path\n            d={`M ${72} ${GROUND_Y - 22}\n                L ${80} ${bodyY - 4}\n                L ${196} ${bodyY - 12}\n                L ${200} ${GROUND_Y - 20}\n                L ${200} ${GROUND_Y - 12}\n                L ${74} ${GROUND_Y - 12} Z`}\n            className=\"fill-muted\"\n            {...line}\n          />\n          {/* big high-flotation tyres (larger radius, drawn inline) */}\n          {[36, 130, 168].map((cx) => (\n            <g key={cx} className=\"text-foreground\">\n              <circle cx={cx} cy={GROUND_Y - 11} r={11} className=\"fill-muted\" stroke=\"currentColor\" strokeWidth={STROKE} vectorEffect=\"non-scaling-stroke\" />\n              <circle cx={cx} cy={GROUND_Y - 11} r={5} className=\"fill-card\" stroke=\"currentColor\" strokeWidth={STROKE * 0.75} vectorEffect=\"non-scaling-stroke\" />\n              <circle cx={cx} cy={GROUND_Y - 11} r={1.4} className=\"fill-foreground\" />\n            </g>\n          ))}\n        </g>\n      )\n    }\n    case \"truck-mixer\": {\n      // 8-wheel rigid carrying an inclined rotating drum + discharge chute.\n      const chassisY = GROUND_Y - 20\n      return (\n        <g className=\"text-foreground\">\n          <Cab frontX={18} rearX={52} roofY={GROUND_Y - 30} />\n          <line x1={52} y1={chassisY} x2={180} y2={chassisY} {...line} strokeWidth={STROKE * 0.75} />\n          {/* front pedestal supporting the high end of the drum */}\n          <line x1={64} y1={chassisY} x2={64} y2={GROUND_Y - 40} {...line} strokeWidth={STROKE * 0.75} />\n          {/* inclined mixer drum: barrel high at the front, tapering to a cone\n              at the rear discharge end */}\n          <path\n            d={`M ${62} ${GROUND_Y - 44}\n                Q ${82} ${GROUND_Y - 52} ${104} ${GROUND_Y - 50}\n                L ${150} ${GROUND_Y - 42}\n                L ${164} ${GROUND_Y - 30}\n                Q ${150} ${GROUND_Y - 26} ${120} ${GROUND_Y - 28}\n                Q ${82} ${GROUND_Y - 30} ${62} ${GROUND_Y - 44} Z`}\n            className=\"fill-muted\"\n            {...line}\n          />\n          {/* drum spiral banding */}\n          <path d={`M ${92} ${GROUND_Y - 51} L ${100} ${GROUND_Y - 30}`} className=\"fill-none\" {...line} strokeWidth={STROKE * 0.5} />\n          <path d={`M ${124} ${GROUND_Y - 47} L ${132} ${GROUND_Y - 28}`} className=\"fill-none\" {...line} strokeWidth={STROKE * 0.5} />\n          {/* discharge chute hanging off the rear cone */}\n          <path d={`M ${164} ${GROUND_Y - 30} L ${182} ${GROUND_Y - 14} L ${174} ${GROUND_Y - 12} Z`} className=\"fill-muted\" {...line} strokeWidth={STROKE * 0.75} />\n          <Wheel cx={36} />\n          <Wheel cx={54} />\n          <Wheel cx={150} />\n          <Wheel cx={168} />\n        </g>\n      )\n    }\n    case \"tractor-trailer\": {\n      // Agricultural tractor (big rear wheel, small front) + tipping trailer.\n      const trTop = GROUND_Y - 34\n      const chassisY = GROUND_Y - 18\n      return (\n        <g className=\"text-foreground\">\n          {/* tractor body + cab */}\n          <path\n            d={`M ${16} ${GROUND_Y - 12}\n                L ${16} ${GROUND_Y - 26}\n                L ${30} ${GROUND_Y - 26}\n                L ${34} ${GROUND_Y - 46}\n                L ${54} ${GROUND_Y - 46}\n                L ${58} ${GROUND_Y - 26}\n                L ${64} ${GROUND_Y - 22}`}\n            className=\"fill-card\"\n            {...line}\n          />\n          <line x1={40} y1={GROUND_Y - 44} x2={52} y2={GROUND_Y - 44} {...line} strokeWidth={STROKE * 0.6} />\n          {/* drawbar to trailer */}\n          <line x1={64} y1={GROUND_Y - 16} x2={86} y2={chassisY} {...line} strokeWidth={STROKE * 0.75} />\n          {/* raked tipping trailer body on a tandem bogie */}\n          <line x1={86} y1={chassisY} x2={210} y2={chassisY} {...line} strokeWidth={STROKE * 0.75} />\n          <path\n            d={`M ${92} ${chassisY - 2}\n                L ${92} ${trTop + 4}\n                L ${206} ${trTop}\n                L ${206} ${chassisY - 2} Z`}\n            className=\"fill-muted\"\n            {...line}\n          />\n          {/* tractor wheels: small front, large rear */}\n          <g className=\"text-foreground\">\n            <circle cx={28} cy={GROUND_Y - 6} r={6} className=\"fill-muted\" stroke=\"currentColor\" strokeWidth={STROKE} vectorEffect=\"non-scaling-stroke\" />\n            <circle cx={28} cy={GROUND_Y - 6} r={2} className=\"fill-card\" stroke=\"currentColor\" strokeWidth={STROKE * 0.6} vectorEffect=\"non-scaling-stroke\" />\n          </g>\n          <g className=\"text-foreground\">\n            <circle cx={52} cy={GROUND_Y - 13} r={13} className=\"fill-muted\" stroke=\"currentColor\" strokeWidth={STROKE} vectorEffect=\"non-scaling-stroke\" />\n            <circle cx={52} cy={GROUND_Y - 13} r={6} className=\"fill-card\" stroke=\"currentColor\" strokeWidth={STROKE * 0.75} vectorEffect=\"non-scaling-stroke\" />\n            <circle cx={52} cy={GROUND_Y - 13} r={1.5} className=\"fill-foreground\" />\n          </g>\n          <Wheel cx={176} />\n          <Wheel cx={194} />\n        </g>\n      )\n    }\n    default:\n      return null\n  }\n}\n\n/** Public component: draws a themed side-profile silhouette for one vehicle. */\nexport function VehicleArtwork({ vehicle, vehicleKey, className }: VehicleArtworkProps) {\n  const key = vehicle?.key ?? vehicleKey\n  if (!key || !(key in VIEW_W)) return null\n  const width = VIEW_W[key]\n  const metres = METRES[key]\n  const label = vehicle?.name ?? key\n\n  return (\n    <svg\n      viewBox={`0 0 ${width} 100`}\n      role=\"img\"\n      aria-label={`Side profile of a ${label}`}\n      className={cn(\"h-auto w-full\", className)}\n      preserveAspectRatio=\"xMidYMid meet\"\n    >\n      <Ground width={width} metres={metres} />\n      <HumanScale x={9} />\n      <Profile vKey={key} width={width} />\n    </svg>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/blocks/domain/haulage-vehicle-select/vehicle-artwork.tsx"
    }
  ],
  "type": "registry:block"
}