{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ewc-selector",
  "title": "Ewc Selector",
  "description": "The European Waste Catalogue picker: a hybrid segmented code entry and search-by-description dropdown, plus a full List of Wastes lookup modal (chapter navigation, WM3 mirror/hazard classification, evidence previews) behind an inline lookup field.",
  "dependencies": [
    "@untitledui/icons@^0.0.22"
  ],
  "registryDependencies": [
    "@nx-ui/badge",
    "@nx-ui/button",
    "@nx-ui/data-ewc-codes",
    "@nx-ui/dialog",
    "@nx-ui/input",
    "@nx-ui/kbd",
    "@nx-ui/label",
    "@nx-ui/utils"
  ],
  "files": [
    {
      "path": "components/blocks/domain/ewc-selector/data.ts",
      "content": "import rawData from \"@/lib/data/ewc-codes.json\"\n\nimport type { EwcEntry } from \"./types\"\n\n/**\n * The full European Waste Catalogue (List of Waste, 2014/955/EU) flattened to a\n * single array — 973 entries (20 chapters + 111 subcategories + 842 leaf codes).\n * Shipped verbatim as `lib/data/ewc-codes.json`; it is load-bearing.\n */\nexport const EWC_CODES: EwcEntry[] = rawData as EwcEntry[]\n",
      "type": "registry:component",
      "target": "components/blocks/domain/ewc-selector/data.ts"
    },
    {
      "path": "components/blocks/domain/ewc-selector/ewc-classify.ts",
      "content": "import { EWC_CODES } from \"./data\"\nimport type { EwcEntry } from \"./types\"\n\n/**\n * WM3-style classification of the List of Waste, derived from the dataset\n * itself at module load (no extra data shipped):\n *\n * - **mirror non-hazardous** — a non-hazardous leaf whose description reads\n *   \"… other than those mentioned in 17 05 03\" (the LoW's mirror-pair\n *   phrasing). Usable only once an assessment has ruled the hazardous pair out.\n * - **mirror hazardous** — a hazardous leaf referenced by one of those mirror\n *   descriptions. Usable only once contamination is confirmed.\n * - **absolute hazardous** — hazardous with no mirror pair; hazardous however\n *   the material assesses.\n * - **non-hazardous** — everything else.\n */\nexport type EwcHazardClass =\n  | \"absolute-hazardous\"\n  | \"mirror-hazardous\"\n  | \"mirror-non-hazardous\"\n  | \"non-hazardous\"\n\n/** The filter vocabulary used by the lookup modal's count pills. */\nexport type EwcLookupFilter = \"all\" | \"non-hazardous\" | \"mirror\" | \"hazardous\"\n\nconst MIRROR_PHRASE = /other than th(?:o|e)se mentioned in/i\nconst CODE_RE = /\\d{2} \\d{2} \\d{2}/g\n\nconst LEAF_CODES = EWC_CODES.filter((e) => e.level === \"code\")\n\n/** code → the hazardous pair codes its mirror description references. */\nconst mirrorPairsOf = new Map<string, string[]>()\n/** hazardous code → the mirror non-hazardous codes that reference it. */\nconst referencedBy = new Map<string, string[]>()\n\nfor (const entry of LEAF_CODES) {\n  if (entry.isHazardous || !MIRROR_PHRASE.test(entry.description)) continue\n  const referenced = (entry.description.match(CODE_RE) ?? []).filter(\n    (code) => code !== entry.code\n  )\n  if (referenced.length === 0) continue\n  mirrorPairsOf.set(entry.code, referenced)\n  for (const code of referenced) {\n    const list = referencedBy.get(code) ?? []\n    list.push(entry.code)\n    referencedBy.set(code, list)\n  }\n}\n\n/** Classify a leaf entry into its WM3 hazard class. */\nexport function classifyEwc(entry: EwcEntry): EwcHazardClass {\n  if (entry.isHazardous) {\n    return referencedBy.has(entry.code) ? \"mirror-hazardous\" : \"absolute-hazardous\"\n  }\n  return mirrorPairsOf.has(entry.code) ? \"mirror-non-hazardous\" : \"non-hazardous\"\n}\n\n/** True when the entry sits on either side of a mirror pair. */\nexport function isMirrorEntry(entry: EwcEntry): boolean {\n  return mirrorPairsOf.has(entry.code) || referencedBy.has(entry.code)\n}\n\n/**\n * The codes on the other side of an entry's mirror pair — the hazardous codes\n * a mirror non-hazardous entry rules out, or the non-hazardous codes a mirror\n * hazardous entry is referenced by. Empty for absolute entries.\n */\nexport function mirrorPairCodes(entry: EwcEntry): string[] {\n  return mirrorPairsOf.get(entry.code) ?? referencedBy.get(entry.code) ?? []\n}\n\nexport function matchesEwcFilter(entry: EwcEntry, filter: EwcLookupFilter): boolean {\n  switch (filter) {\n    case \"all\":\n      return true\n    case \"hazardous\":\n      return entry.isHazardous\n    case \"non-hazardous\":\n      return !entry.isHazardous\n    case \"mirror\":\n      return isMirrorEntry(entry)\n  }\n}\n\n/**\n * The evidence a movement under this code will require — the consequence\n * preview shown in the lookup modal's detail pane. Deliberately hardcoded per\n * hazard class (mirrors the Digital Waste Tracking / duty-of-care baseline).\n */\nexport function evidenceForEntry(entry: EwcEntry): string[] {\n  switch (classifyEwc(entry)) {\n    case \"absolute-hazardous\":\n    case \"mirror-hazardous\":\n      return [\n        \"Hazardous waste consignment note per load\",\n        \"Producer premises code\",\n        \"Pre-acceptance audit trail with the receiver\",\n        \"Receiver permit accepting this hazardous EWC\",\n        \"Net weight and container count per load\",\n      ]\n    case \"mirror-non-hazardous\":\n      return [\n        \"Classification report justifying non-hazardous status\",\n        \"Waste transfer note (or season ticket)\",\n        \"Carrier registration\",\n        \"Receiver permit or exemption\",\n        \"Net weight per load\",\n      ]\n    case \"non-hazardous\":\n      return [\n        \"Waste transfer note\",\n        \"Carrier registration\",\n        \"Receiver permit or exemption\",\n        \"Net weight per load\",\n      ]\n  }\n}\n\nexport interface EwcChapterSummary {\n  /** Two-digit chapter code, e.g. \"17\". */\n  code: string\n  /** Chapter heading, sentence-cased from the dataset's uppercase original. */\n  label: string\n  /** Number of leaf codes in the chapter. */\n  count: number\n  /** Flagged as the primary chapter for construction movements. */\n  primary?: boolean\n}\n\n/** Sentence-case an UPPERCASE chapter heading for UI display. */\nfunction sentenceCase(text: string): string {\n  const lower = text.toLowerCase()\n  return lower.charAt(0).toUpperCase() + lower.slice(1)\n}\n\n/** All 20 chapters with their leaf-code counts, chapter 17 flagged primary. */\nexport const EWC_CHAPTERS: EwcChapterSummary[] = EWC_CODES.filter(\n  (e) => e.level === \"chapter\"\n).map((chapter) => ({\n  code: chapter.code,\n  label: sentenceCase(chapter.description),\n  count: LEAF_CODES.filter((e) => e.code.startsWith(chapter.code)).length,\n  ...(chapter.code === \"17\" ? { primary: true } : {}),\n}))\n\n/** Every leaf (selectable) code, in catalogue order. */\nexport const EWC_LEAF_CODES: EwcEntry[] = LEAF_CODES\n",
      "type": "registry:component",
      "target": "components/blocks/domain/ewc-selector/ewc-classify.ts"
    },
    {
      "path": "components/blocks/domain/ewc-selector/ewc-code-detail.tsx",
      "content": "import { cn } from \"@/lib/utils\"\n\nimport type { EwcCodeDetailProps } from \"./types\"\n\n/**\n * EwcCodeDetail — a boxed detail card for a full `EwcEntry`: the mono code, a\n * Hazardous badge, the description, and the material category.\n */\nexport function EwcCodeDetail({ entry, className }: EwcCodeDetailProps) {\n  return (\n    <div className={cn(\"rounded-md border border-border bg-muted p-4\", className)}>\n      <div className=\"flex items-start justify-between gap-3\">\n        <span className=\"font-mono text-sm font-semibold tracking-wide\">\n          {entry.code}\n        </span>\n        {entry.isHazardous && (\n          <span className=\"shrink-0 rounded-md bg-destructive/10 px-2 py-0.5 text-xs font-medium text-destructive\">\n            Hazardous\n          </span>\n        )}\n      </div>\n      <p className=\"mt-1.5 text-sm text-foreground\">{entry.description}</p>\n      {entry.material && (\n        <p className=\"mt-1 text-xs text-muted-foreground\">\n          <span className=\"font-medium\">Material category:</span> {entry.material}\n        </p>\n      )}\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/blocks/domain/ewc-selector/ewc-code-detail.tsx"
    },
    {
      "path": "components/blocks/domain/ewc-selector/ewc-code-summary.tsx",
      "content": "\"use client\"\n\nimport { cn } from \"@/lib/utils\"\n\nimport { HazardousBadge } from \"./ewc-result-row\"\nimport { findEwcEntry } from \"./helpers\"\n\ninterface EwcCodeSummaryProps {\n  /** A full EWC code string, e.g. \"17 06 01\". */\n  code: string\n  className?: string\n}\n\n/**\n * EwcCodeSummary — a read-only display of an EWC code: the code (with a `*`\n * suffix and a Hazardous badge when hazardous) and its description below. Shows\n * an em-dash placeholder when `code` is empty.\n */\nexport function EwcCodeSummary({ code, className }: EwcCodeSummaryProps) {\n  const entry = findEwcEntry(code)\n  const displayCode = entry?.isHazardous ? `${code}*` : code\n\n  if (!code) {\n    return (\n      <div className={className}>\n        <span className=\"text-sm text-muted-foreground italic\">—</span>\n      </div>\n    )\n  }\n\n  return (\n    <div className={cn(className)}>\n      <div className=\"flex items-center gap-2\">\n        <span className=\"font-mono text-sm font-semibold tracking-wide\">\n          {displayCode}\n        </span>\n        {entry?.isHazardous && <HazardousBadge />}\n      </div>\n      {entry?.description && (\n        <p className=\"mt-0.5 text-xs text-muted-foreground\">{entry.description}</p>\n      )}\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/blocks/domain/ewc-selector/ewc-code-summary.tsx"
    },
    {
      "path": "components/blocks/domain/ewc-selector/ewc-input-bar.tsx",
      "content": "\"use client\"\n\nimport type { KeyboardEvent, RefObject } from \"react\"\nimport { XClose } from \"@untitledui/icons\"\n\nimport { cn } from \"@/lib/utils\"\n\nimport { EwcIcon, EwcSegmentInput, SegmentSeparator } from \"./ewc-segment-input\"\nimport type { EwcEntry, Segments } from \"./types\"\n\ninterface EwcInputBarProps {\n  segments: Segments\n  selectedEntry: EwcEntry | null\n  hasValue: boolean\n  disabled: boolean\n  open: boolean\n  highlightedIndex: number\n  activeEntries: EwcEntry[]\n  listboxId: string\n  segmentRefs: RefObject<(HTMLInputElement | null)[]>\n  onSegmentChange: (index: number, val: string) => void\n  onSegmentFocus: () => void\n  onSegmentKeyDown: (index: number, e: KeyboardEvent<HTMLInputElement>) => void\n  onPaste: (e: React.ClipboardEvent<HTMLInputElement>) => void\n  onClear: () => void\n  onFieldsetMouseDown: (e: React.MouseEvent<HTMLFieldSetElement>) => void\n}\n\n/** The `00 · 00 · 00` input row: hash icon, three segments, hazardous `*`, clear. */\nexport function EwcInputBar({\n  segments,\n  selectedEntry,\n  hasValue,\n  disabled,\n  open,\n  highlightedIndex,\n  activeEntries,\n  listboxId,\n  segmentRefs,\n  onSegmentChange,\n  onSegmentFocus,\n  onSegmentKeyDown,\n  onPaste,\n  onClear,\n  onFieldsetMouseDown,\n}: EwcInputBarProps) {\n  return (\n    <fieldset\n      className={cn(\n        \"m-0 flex h-10 w-full items-center gap-1.5 rounded-lg border bg-background px-3 shadow-xs transition-all\",\n        disabled ? \"cursor-not-allowed opacity-50\" : \"cursor-text\",\n        open\n          ? \"border-ring ring-4 ring-ring/25\"\n          : \"border-input hover:border-muted-foreground/30\"\n      )}\n      disabled={disabled}\n      onMouseDown={onFieldsetMouseDown}\n    >\n      <legend className=\"sr-only\">EWC code</legend>\n      <EwcIcon />\n\n      <div className=\"flex items-center gap-1\">\n        {([0, 1, 2] as const).map((i) => (\n          <span className=\"contents\" key={i}>\n            {i > 0 && <SegmentSeparator />}\n            <EwcSegmentInput\n              activeEntries={activeEntries}\n              disabled={disabled}\n              highlightedIndex={highlightedIndex}\n              index={i}\n              listboxId={listboxId}\n              onChange={onSegmentChange}\n              onFocus={onSegmentFocus}\n              onKeyDown={onSegmentKeyDown}\n              onPaste={onPaste}\n              open={open}\n              segmentRef={(el) => {\n                segmentRefs.current[i] = el\n              }}\n              value={segments[i]}\n            />\n          </span>\n        ))}\n        {selectedEntry?.isHazardous && (\n          <span\n            aria-hidden\n            className=\"font-mono text-sm font-bold text-destructive\"\n            title=\"Hazardous waste code\"\n          >\n            *\n          </span>\n        )}\n      </div>\n\n      {hasValue && !disabled && (\n        <button\n          aria-label=\"Clear selection\"\n          className=\"ml-auto rounded-md p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground\"\n          onClick={(e) => {\n            e.stopPropagation()\n            onClear()\n          }}\n          tabIndex={-1}\n          type=\"button\"\n        >\n          <XClose className=\"size-4\" />\n        </button>\n      )}\n    </fieldset>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/blocks/domain/ewc-selector/ewc-input-bar.tsx"
    },
    {
      "path": "components/blocks/domain/ewc-selector/ewc-lookup-field.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { AlertTriangle, CheckCircle, SearchLg, XClose } from \"@untitledui/icons\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"@/components/ui/button\"\nimport { Input } from \"@/components/ui/input\"\nimport { Label } from \"@/components/ui/label\"\n\nimport { classifyEwc } from \"./ewc-classify\"\nimport { EwcLookupModal } from \"./ewc-lookup-modal\"\nimport { findEwcEntry } from \"./helpers\"\nimport type { EwcEntry } from \"./types\"\n\nexport interface EwcLookupFieldProps {\n  /** The current full code, e.g. \"17 05 04\", or `null` when unset. */\n  value: string | null\n  /** Fires with the resolved entry, or `null` when cleared. */\n  onChange: (entry: EwcEntry | null) => void\n  label?: string\n  /** Marks the label \"(optional)\" — e.g. while the material status is unknown. */\n  optional?: boolean\n  /** Helper copy under the input row. Pass `null` to hide. */\n  description?: React.ReactNode | null\n  /**\n   * Copy for the hazardous auto-upgrade banner shown when a hazardous code is\n   * chosen. Pass `null` to hide, or omit for the class-aware default.\n   */\n  hazardousNotice?: React.ReactNode | null\n  /** Shown in the lookup modal's detail pane for sanity-checking a code. */\n  materialContext?: { material?: string; status?: string }\n  disabled?: boolean\n  invalid?: boolean\n  className?: string\n}\n\n/** Normalise free typing (\"170504\", \"17.05.04\", \"17 05 04*\") to \"17 05 04\". */\nfunction normaliseCodeInput(raw: string): string {\n  const digits = raw.replace(/\\D/g, \"\").slice(0, 6)\n  return digits.replace(/(\\d{2})(?=\\d)/g, \"$1 \").trim()\n}\n\n/**\n * EwcLookupField — the movement-form EWC entry: a mono code input beside a\n * \"Look up\" button that opens the full `EwcLookupModal`. Typing a complete,\n * valid six-digit code resolves it inline (no modal needed); a resolved code\n * shows a confirmation strip, and hazardous codes surface an explanatory\n * auto-upgrade banner so forms can flip their status with no surprises.\n *\n * Controlled: pass `value` (e.g. \"17 05 04\") and handle `onChange`, which\n * fires with the resolved `EwcEntry` (see `entry.isHazardous`) or `null`.\n */\nexport function EwcLookupField({\n  value,\n  onChange,\n  label = \"EWC code\",\n  optional = false,\n  description = (\n    <>\n      Used on waste paperwork (WTNs, consignment notes). Codes ending in{\" \"}\n      <span className=\"font-mono\">*</span> are hazardous.\n    </>\n  ),\n  hazardousNotice,\n  materialContext,\n  disabled = false,\n  invalid = false,\n  className,\n}: EwcLookupFieldProps) {\n  const [draft, setDraft] = React.useState(value ?? \"\")\n  const [modalOpen, setModalOpen] = React.useState(false)\n  const inputId = React.useId()\n\n  const entry = React.useMemo(() => findEwcEntry(value), [value])\n\n  // Keep the draft in step with the controlled value (e.g. modal picks,\n  // parent-driven cascades), without clobbering in-progress typing.\n  React.useEffect(() => {\n    setDraft(value ? (findEwcEntry(value)?.isHazardous ? `${value}*` : value) : \"\")\n  }, [value])\n\n  const handleDraftChange = (raw: string) => {\n    setDraft(raw)\n    const normalised = normaliseCodeInput(raw)\n    if (normalised.length === 8) {\n      const match = findEwcEntry(normalised)\n      if (match) {\n        onChange(match)\n        return\n      }\n    }\n    if (!raw.trim() && value) onChange(null)\n  }\n\n  const handlePick = (picked: EwcEntry) => {\n    onChange(picked)\n  }\n\n  const hazardClass = entry ? classifyEwc(entry) : null\n  const showHazardBanner =\n    entry?.isHazardous === true && hazardousNotice !== null\n\n  return (\n    <div className={cn(\"flex flex-col gap-1.5\", className)} data-slot=\"ewc-lookup-field\">\n      <Label htmlFor={inputId}>\n        {label}\n        {optional && (\n          <span className=\"font-normal text-muted-foreground\">(optional)</span>\n        )}\n      </Label>\n      <div className=\"flex gap-2\">\n        <Input\n          id={inputId}\n          value={draft}\n          onChange={(e) => handleDraftChange(e.target.value)}\n          placeholder=\"17 05 04\"\n          inputMode=\"numeric\"\n          autoComplete=\"off\"\n          disabled={disabled}\n          aria-invalid={invalid || undefined}\n          className=\"max-w-40 font-mono text-sm\"\n        />\n        <Button\n          type=\"button\"\n          variant=\"outline\"\n          onClick={() => setModalOpen(true)}\n          disabled={disabled}\n          data-slot=\"ewc-lookup-open\"\n        >\n          <SearchLg data-icon=\"inline-start\" />\n          Look up\n        </Button>\n      </div>\n      {description ? (\n        <p className=\"text-xs text-muted-foreground\">{description}</p>\n      ) : null}\n\n      {entry && (\n        <div className=\"mt-0.5 flex items-center gap-2 rounded-lg border border-utility-green-200 bg-utility-green-50 px-3 py-2 text-xs leading-snug text-utility-green-700\">\n          <CheckCircle aria-hidden className=\"size-3.5 shrink-0\" />\n          <span className=\"min-w-0 flex-1 truncate font-medium\">\n            {entry.description}\n          </span>\n          <button\n            type=\"button\"\n            aria-label=\"Clear EWC code\"\n            onClick={() => onChange(null)}\n            className=\"shrink-0 rounded-sm text-utility-green-700/70 transition-colors hover:text-utility-green-700\"\n          >\n            <XClose className=\"size-3.5\" />\n          </button>\n        </div>\n      )}\n\n      {showHazardBanner && (\n        <div\n          className=\"flex items-start gap-2 rounded-lg border border-utility-yellow-200 bg-utility-yellow-50 px-3 py-2 text-xs leading-relaxed text-utility-yellow-700\"\n          data-slot=\"ewc-hazard-banner\"\n        >\n          <AlertTriangle aria-hidden className=\"mt-0.5 size-3.5 shrink-0\" />\n          <p>\n            {hazardousNotice ??\n              (hazardClass === \"mirror-hazardous\" ? (\n                <>\n                  This EWC code is <strong>mirror hazardous</strong> — keep the WM3\n                  classification that confirmed contamination. Status upgrades to\n                  hazardous waste and consignment paperwork is added.\n                </>\n              ) : (\n                <>\n                  This EWC code is <strong>absolute hazardous</strong>. Status\n                  upgrades to hazardous waste and consignment paperwork is added.\n                </>\n              ))}\n          </p>\n        </div>\n      )}\n\n      <EwcLookupModal\n        open={modalOpen}\n        onOpenChange={setModalOpen}\n        onSelect={handlePick}\n        initialQuery={value ?? normaliseCodeInput(draft)}\n        materialContext={materialContext}\n      />\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/blocks/domain/ewc-selector/ewc-lookup-field.tsx"
    },
    {
      "path": "components/blocks/domain/ewc-selector/ewc-lookup-modal.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  AlertTriangle,\n  Check,\n  CheckCircle,\n  File02,\n  InfoCircle,\n  SearchLg,\n} from \"@untitledui/icons\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog\"\nimport { Kbd, KbdGroup } from \"@/components/ui/kbd\"\n\nimport {\n  classifyEwc,\n  evidenceForEntry,\n  EWC_CHAPTERS,\n  EWC_LEAF_CODES,\n  isMirrorEntry,\n  matchesEwcFilter,\n  mirrorPairCodes,\n  type EwcLookupFilter,\n} from \"./ewc-classify\"\nimport { loadRecentCodes, saveRecentCode } from \"./helpers\"\nimport type { EwcEntry } from \"./types\"\n\nexport interface EwcLookupModalProps {\n  open: boolean\n  onOpenChange: (open: boolean) => void\n  /** Fires with the chosen leaf entry; the modal closes itself after. */\n  onSelect: (entry: EwcEntry) => void\n  /** Seed the search box, e.g. with the code typed in the field so far. */\n  initialQuery?: string\n  /** Shown in the detail pane so the picker can be sanity-checked against the movement. */\n  materialContext?: { material?: string; status?: string }\n}\n\nconst FILTERS: { key: EwcLookupFilter; label: string }[] = [\n  { key: \"all\", label: \"All\" },\n  { key: \"non-hazardous\", label: \"Non-hazardous\" },\n  { key: \"mirror\", label: \"Mirror\" },\n  { key: \"hazardous\", label: \"Hazardous\" },\n]\n\n/** Rendering cap for very broad views (no query, no chapter). */\nconst MAX_ROWS = 320\n\n/**\n * EwcLookupModal — the full-screen List of Wastes lookup: search, hazard-class\n * count pills, a chapter sidebar with recently-used codes, grouped code rows\n * with WM3 flag badges, and a detail pane that previews the mirror/hazardous\n * warnings and the evidence a code will require before commit.\n *\n * Controlled: pass `open`/`onOpenChange`; a pick fires `onSelect` with the\n * leaf `EwcEntry` (check `isHazardous` to drive status auto-upgrade) and\n * records the code in the shared recently-used store.\n */\nexport function EwcLookupModal({\n  open,\n  onOpenChange,\n  onSelect,\n  initialQuery,\n  materialContext,\n}: EwcLookupModalProps) {\n  const [query, setQuery] = React.useState(\"\")\n  const [filter, setFilter] = React.useState<EwcLookupFilter>(\"all\")\n  const [chapter, setChapter] = React.useState<string | null>(\"17\")\n  const [selectedCode, setSelectedCode] = React.useState<string | null>(null)\n  const [recents, setRecents] = React.useState<EwcEntry[]>([])\n\n  const searchRef = React.useRef<HTMLInputElement>(null)\n  const listRef = React.useRef<HTMLDivElement>(null)\n\n  // Reset per open, seeding the search with whatever the field held.\n  React.useEffect(() => {\n    if (!open) return\n    const seed = initialQuery?.trim() ?? \"\"\n    setQuery(seed)\n    setFilter(\"all\")\n    setChapter(seed ? null : \"17\")\n    setSelectedCode(null)\n    setRecents(loadRecentCodes())\n  }, [open, initialQuery])\n\n  const trimmedQuery = query.trim().toLowerCase()\n\n  // Scope: chapter narrows the browse view; a query searches the whole\n  // catalogue (and ignores the chapter, so search never comes back empty\n  // because of a stale chapter pick).\n  const scoped = React.useMemo(() => {\n    let pool = EWC_LEAF_CODES\n    if (trimmedQuery) {\n      pool = pool.filter(\n        (e) =>\n          e.code.includes(trimmedQuery) ||\n          e.description.toLowerCase().includes(trimmedQuery) ||\n          (e.material?.toLowerCase().includes(trimmedQuery) ?? false)\n      )\n    } else if (chapter) {\n      pool = pool.filter((e) => e.code.startsWith(chapter))\n    }\n    return pool\n  }, [trimmedQuery, chapter])\n\n  const counts = React.useMemo(() => {\n    const result = { all: scoped.length, \"non-hazardous\": 0, mirror: 0, hazardous: 0 }\n    for (const entry of scoped) {\n      if (entry.isHazardous) result.hazardous += 1\n      else result[\"non-hazardous\"] += 1\n      if (isMirrorEntry(entry)) result.mirror += 1\n    }\n    return result\n  }, [scoped])\n\n  const visible = React.useMemo(\n    () => scoped.filter((e) => matchesEwcFilter(e, filter)).slice(0, MAX_ROWS),\n    [scoped, filter]\n  )\n  const totalMatching = React.useMemo(\n    () => scoped.filter((e) => matchesEwcFilter(e, filter)).length,\n    [scoped, filter]\n  )\n\n  // Group the visible rows by chapter for captioned sections.\n  const groups = React.useMemo(() => {\n    const byChapter = new Map<string, EwcEntry[]>()\n    for (const entry of visible) {\n      const key = entry.code.slice(0, 2)\n      const list = byChapter.get(key) ?? []\n      list.push(entry)\n      byChapter.set(key, list)\n    }\n    return [...byChapter.entries()].map(([code, entries]) => ({\n      chapter: EWC_CHAPTERS.find((c) => c.code === code),\n      code,\n      entries,\n    }))\n  }, [visible])\n\n  const selectedEntry = React.useMemo(\n    () => visible.find((e) => e.code === selectedCode) ?? EWC_LEAF_CODES.find((e) => e.code === selectedCode) ?? null,\n    [visible, selectedCode]\n  )\n\n  const commit = React.useCallback(\n    (entry: EwcEntry) => {\n      saveRecentCode(entry)\n      onSelect(entry)\n      onOpenChange(false)\n    },\n    [onSelect, onOpenChange]\n  )\n\n  // Keyboard flow from the search box: arrows walk the visible rows, Enter\n  // selects the highlighted row (first press) or commits it (already selected).\n  const handleSearchKeyDown = React.useCallback(\n    (e: React.KeyboardEvent<HTMLInputElement>) => {\n      if (e.key !== \"ArrowDown\" && e.key !== \"ArrowUp\" && e.key !== \"Enter\") return\n      if (visible.length === 0) return\n      const index = selectedCode\n        ? visible.findIndex((entry) => entry.code === selectedCode)\n        : -1\n      if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n        e.preventDefault()\n        const next =\n          e.key === \"ArrowDown\"\n            ? Math.min(index + 1, visible.length - 1)\n            : Math.max(index - 1, 0)\n        const entry = visible[next]\n        if (entry) {\n          setSelectedCode(entry.code)\n          listRef.current\n            ?.querySelector(`[data-code=\"${entry.code}\"]`)\n            ?.scrollIntoView({ block: \"nearest\" })\n        }\n      } else if (e.key === \"Enter\") {\n        e.preventDefault()\n        const entry = index >= 0 ? visible[index] : visible[0]\n        if (!entry) return\n        if (entry.code === selectedCode) commit(entry)\n        else setSelectedCode(entry.code)\n      }\n    },\n    [visible, selectedCode, commit]\n  )\n\n  const activeChapter = trimmedQuery ? null : chapter\n\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogContent\n        className=\"flex h-[min(720px,85vh)] flex-col gap-0 overflow-hidden p-0 sm:max-w-6xl\"\n        initialFocus={searchRef}\n      >\n        {/* Header */}\n        <DialogHeader className=\"flex-row items-center gap-3 border-b border-border px-5 py-4 pr-14\">\n          <span\n            aria-hidden\n            className=\"flex size-9 shrink-0 items-center justify-center rounded-lg bg-utility-brand-50 text-utility-brand-700\"\n          >\n            <File02 className=\"size-4.5\" />\n          </span>\n          <div className=\"flex min-w-0 flex-col gap-0.5\">\n            <DialogTitle className=\"text-md\">EWC / List of Wastes lookup</DialogTitle>\n            <DialogDescription className=\"truncate text-xs\">\n              Pick the six-digit code that best classifies this material movement.\n            </DialogDescription>\n          </div>\n        </DialogHeader>\n\n        {/* Search + hazard-class pills */}\n        <div className=\"flex flex-wrap items-center gap-2 border-b border-border px-5 py-3\">\n          <div className=\"relative min-w-56 flex-1\">\n            <SearchLg\n              aria-hidden\n              className=\"pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground\"\n            />\n            <input\n              ref={searchRef}\n              type=\"text\"\n              value={query}\n              onChange={(e) => setQuery(e.target.value)}\n              onKeyDown={handleSearchKeyDown}\n              placeholder=\"Search by code or keyword (e.g. 17 05, topsoil, asbestos)…\"\n              aria-label=\"Search the List of Wastes\"\n              className=\"h-9 w-full rounded-lg border border-input bg-background pr-3 pl-9 text-sm shadow-xs outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-4 focus-visible:ring-ring/25\"\n              data-slot=\"ewc-lookup-search\"\n            />\n          </div>\n          <div className=\"flex flex-wrap items-center gap-1.5\" role=\"group\" aria-label=\"Hazard class filter\">\n            {FILTERS.map(({ key, label }) => (\n              <button\n                key={key}\n                type=\"button\"\n                aria-pressed={filter === key}\n                onClick={() => setFilter(key)}\n                className={cn(\n                  \"inline-flex h-7 items-center gap-1.5 rounded-full border px-2.5 text-xs font-medium transition-colors\",\n                  filter === key\n                    ? \"border-utility-brand-200 bg-utility-brand-50 text-utility-brand-700\"\n                    : \"border-border bg-background text-muted-foreground hover:bg-muted hover:text-foreground\"\n                )}\n              >\n                {label}\n                <span className=\"font-mono text-[11px] tabular-nums opacity-70\">\n                  {counts[key]}\n                </span>\n              </button>\n            ))}\n          </div>\n        </div>\n\n        {/* Body: sidebar · results · detail */}\n        <div className=\"flex min-h-0 flex-1\">\n          {/* Sidebar — recents + chapters */}\n          <div className=\"hidden w-52 shrink-0 flex-col overflow-y-auto border-r border-border py-3 md:flex\">\n            {recents.length > 0 && (\n              <div className=\"px-3 pb-3\">\n                <p className=\"px-1 pb-1.5 text-[11px] font-semibold tracking-wider text-muted-foreground uppercase\">\n                  Recently used\n                </p>\n                <div className=\"flex flex-wrap gap-1\">\n                  {recents.map((entry) => (\n                    <button\n                      key={entry.code}\n                      type=\"button\"\n                      onClick={() => {\n                        setQuery(entry.code)\n                        setSelectedCode(entry.code)\n                      }}\n                      title={entry.description}\n                      className={cn(\n                        \"rounded-md border px-1.5 py-0.5 font-mono text-[11px] font-medium transition-colors\",\n                        selectedCode === entry.code\n                          ? \"border-utility-brand-200 bg-utility-brand-50 text-utility-brand-700\"\n                          : \"border-border bg-background text-muted-foreground hover:bg-muted hover:text-foreground\"\n                      )}\n                    >\n                      {entry.isHazardous ? `${entry.code}*` : entry.code}\n                    </button>\n                  ))}\n                </div>\n              </div>\n            )}\n            <p className=\"px-4 pb-1.5 text-[11px] font-semibold tracking-wider text-muted-foreground uppercase\">\n              Chapters\n            </p>\n            <div className=\"flex flex-col px-2\">\n              {EWC_CHAPTERS.map((ch) => (\n                <button\n                  key={ch.code}\n                  type=\"button\"\n                  aria-pressed={activeChapter === ch.code}\n                  onClick={() => {\n                    // Toggling an active chapter off widens back to the whole list.\n                    setChapter(activeChapter === ch.code ? null : ch.code)\n                    setQuery(\"\")\n                  }}\n                  className={cn(\n                    \"flex items-start gap-2 rounded-md px-2 py-1.5 text-left text-xs transition-colors\",\n                    activeChapter === ch.code\n                      ? \"bg-utility-brand-50 text-utility-brand-700\"\n                      : \"text-muted-foreground hover:bg-muted hover:text-foreground\"\n                  )}\n                  data-slot=\"ewc-lookup-chapter\"\n                >\n                  <span className=\"w-5 shrink-0 pt-px font-mono font-semibold tabular-nums\">\n                    {ch.code}\n                  </span>\n                  <span className=\"min-w-0 flex-1 truncate leading-5\">{ch.label}</span>\n                  {ch.primary && (\n                    <span className=\"shrink-0 pt-px text-[10px] font-semibold tracking-wide text-utility-brand-500 uppercase\">\n                      common\n                    </span>\n                  )}\n                </button>\n              ))}\n            </div>\n          </div>\n\n          {/* Centre — grouped result rows */}\n          <div ref={listRef} className=\"min-w-0 flex-1 overflow-y-auto px-4 py-3\">\n            <p className=\"px-1 pb-2 text-xs text-muted-foreground\">\n              {totalMatching} {totalMatching === 1 ? \"code\" : \"codes\"}\n              {trimmedQuery\n                ? ` matching “${query.trim()}”`\n                : activeChapter\n                  ? ` in chapter ${activeChapter}`\n                  : \" in the catalogue\"}\n              {\" · sorted by code\"}\n              {totalMatching > MAX_ROWS ? ` · showing the first ${MAX_ROWS}` : \"\"}\n            </p>\n\n            {visible.length === 0 && (\n              <div className=\"flex flex-col items-center justify-center gap-2 py-16 text-center\">\n                <p className=\"text-sm font-medium text-foreground\">No matching codes</p>\n                <p className=\"max-w-60 text-xs text-muted-foreground\">\n                  Try a shorter keyword, a code prefix like “17 05”, or clear the\n                  hazard filter.\n                </p>\n                {(trimmedQuery || filter !== \"all\") && (\n                  <Button\n                    variant=\"outline\"\n                    size=\"xs\"\n                    className=\"mt-1\"\n                    onClick={() => {\n                      setQuery(\"\")\n                      setFilter(\"all\")\n                    }}\n                  >\n                    Clear search\n                  </Button>\n                )}\n              </div>\n            )}\n\n            {groups.map((group) => (\n              <div key={group.code} className=\"pb-3\">\n                <p className=\"px-1 pt-1 pb-1.5 text-[11px] font-semibold text-muted-foreground\">\n                  <span className=\"font-mono\">{group.code}</span>\n                  {group.chapter ? ` · ${group.chapter.label}` : null}\n                </p>\n                <div className=\"flex flex-col gap-0.5\">\n                  {group.entries.map((entry) => (\n                    <EwcLookupRow\n                      key={entry.code}\n                      entry={entry}\n                      selected={entry.code === selectedCode}\n                      onSelect={() => setSelectedCode(entry.code)}\n                      onCommit={() => commit(entry)}\n                    />\n                  ))}\n                </div>\n              </div>\n            ))}\n          </div>\n\n          {/* Right — detail pane */}\n          <div className=\"hidden w-80 shrink-0 flex-col border-l border-border bg-muted/30 lg:flex\">\n            {selectedEntry ? (\n              <EwcDetailPane\n                entry={selectedEntry}\n                materialContext={materialContext}\n                onCommit={() => commit(selectedEntry)}\n              />\n            ) : (\n              <div className=\"flex flex-1 flex-col items-center justify-center gap-2 px-8 text-center\">\n                <span className=\"flex size-10 items-center justify-center rounded-lg border border-border bg-background text-muted-foreground\">\n                  <File02 className=\"size-5\" />\n                </span>\n                <p className=\"text-sm font-medium text-foreground\">\n                  Select a code to view details\n                </p>\n                <p className=\"text-xs leading-relaxed text-muted-foreground\">\n                  You&rsquo;ll see the parent chapter, hazardous status, mirror pair,\n                  and which evidence becomes required.\n                </p>\n              </div>\n            )}\n          </div>\n        </div>\n\n        {/* Footer */}\n        <div className=\"flex items-center justify-between gap-3 border-t border-border bg-muted/30 px-5 py-2.5\">\n          <KbdGroup className=\"text-xs text-muted-foreground\">\n            <Kbd>↑</Kbd>\n            <Kbd>↓</Kbd>\n            <span>navigate</span>\n            <Kbd>↵</Kbd>\n            <span>select</span>\n            <Kbd>esc</Kbd>\n            <span>close</span>\n          </KbdGroup>\n          <p className=\"hidden truncate text-[11px] text-muted-foreground sm:block\">\n            List of Wastes (England) Regulations, as amended · Decision 2000/532/EC\n          </p>\n        </div>\n      </DialogContent>\n    </Dialog>\n  )\n}\n\nfunction EwcLookupRow({\n  entry,\n  selected,\n  onSelect,\n  onCommit,\n}: {\n  entry: EwcEntry\n  selected: boolean\n  onSelect: () => void\n  onCommit: () => void\n}) {\n  return (\n    <div\n      data-code={entry.code}\n      data-slot=\"ewc-lookup-row\"\n      className={cn(\n        \"flex items-center gap-3 rounded-lg border px-2.5 py-2 transition-colors\",\n        selected\n          ? \"border-utility-brand-200 bg-utility-brand-50\"\n          : \"border-transparent hover:bg-muted\"\n      )}\n    >\n      <button\n        type=\"button\"\n        onClick={onSelect}\n        onDoubleClick={onCommit}\n        className=\"flex min-w-0 flex-1 items-center gap-3 text-left outline-none focus-visible:underline\"\n        aria-pressed={selected}\n      >\n        <span className=\"w-[86px] shrink-0 font-mono text-sm font-semibold whitespace-nowrap tabular-nums\">\n          {entry.isHazardous ? `${entry.code}*` : entry.code}\n        </span>\n        <span className=\"min-w-0 flex-1\">\n          <span className=\"block truncate text-[13px] text-foreground\">\n            {entry.description}\n          </span>\n        </span>\n        <EwcFlagBadge entry={entry} />\n      </button>\n      {selected && (\n        <Button size=\"xs\" onClick={onCommit} data-slot=\"ewc-lookup-use\">\n          Use code\n          <Check data-icon=\"inline-end\" />\n        </Button>\n      )}\n    </div>\n  )\n}\n\nfunction EwcFlagBadge({ entry }: { entry: EwcEntry }) {\n  if (entry.isHazardous) return <Badge variant=\"error\">Hazardous</Badge>\n  if (isMirrorEntry(entry)) return <Badge variant=\"warning\">Mirror</Badge>\n  return <Badge variant=\"success\">Non-haz</Badge>\n}\n\nfunction EwcDetailPane({\n  entry,\n  materialContext,\n  onCommit,\n}: {\n  entry: EwcEntry\n  materialContext?: { material?: string; status?: string }\n  onCommit: () => void\n}) {\n  const hazardClass = classifyEwc(entry)\n  const pairCodes = mirrorPairCodes(entry)\n  const evidence = evidenceForEntry(entry)\n\n  return (\n    <>\n      <div className=\"min-h-0 flex-1 overflow-y-auto px-4 py-4\">\n        <p className=\"text-[11px] font-semibold tracking-wider text-muted-foreground uppercase\">\n          Chapter {entry.code.slice(0, 2)}\n        </p>\n        <div className=\"mt-1 flex items-center gap-2\">\n          <span className=\"font-mono text-[22px] leading-none font-semibold tracking-tight\">\n            {entry.isHazardous ? `${entry.code}*` : entry.code}\n          </span>\n          <EwcFlagBadge entry={entry} />\n        </div>\n        <p className=\"mt-2 text-sm leading-snug text-foreground\">{entry.description}</p>\n        {entry.material && (\n          <p className=\"mt-1 text-xs text-muted-foreground\">{entry.material}</p>\n        )}\n\n        {hazardClass !== \"non-hazardous\" && (\n          <div\n            className={cn(\n              \"mt-3 flex gap-2 rounded-lg border px-3 py-2.5 text-xs leading-relaxed\",\n              entry.isHazardous\n                ? \"border-utility-red-200 bg-utility-red-50 text-utility-red-700\"\n                : \"border-utility-yellow-200 bg-utility-yellow-50 text-utility-yellow-700\"\n            )}\n          >\n            <AlertTriangle aria-hidden className=\"mt-0.5 size-3.5 shrink-0\" />\n            <p>\n              {hazardClass === \"mirror-hazardous\" &&\n                \"Pick this hazardous code only when contamination has been confirmed by classification (WM3 / hazardous waste assessment).\"}\n              {hazardClass === \"absolute-hazardous\" &&\n                \"Absolute hazardous entry — every load moves under consignment-note procedures, whatever the assessment says.\"}\n              {hazardClass === \"mirror-non-hazardous\" && (\n                <>\n                  Mirror entry — use only when a WM3 assessment has ruled out the\n                  hazardous pair{\" \"}\n                  <span className=\"font-mono font-semibold\">\n                    {pairCodes.map((code) => `${code}*`).join(\", \")}\n                  </span>\n                  .\n                </>\n              )}\n            </p>\n          </div>\n        )}\n\n        {materialContext?.material && (\n          <div className=\"mt-3 flex gap-2 rounded-lg border border-utility-brand-200 bg-utility-brand-50 px-3 py-2.5 text-xs leading-relaxed text-utility-brand-700\">\n            <InfoCircle aria-hidden className=\"mt-0.5 size-3.5 shrink-0\" />\n            <p>\n              Match against the movement — material is{\" \"}\n              <span className=\"font-semibold\">{materialContext.material}</span>\n              {materialContext.status ? (\n                <>\n                  , status <span className=\"font-semibold\">{materialContext.status}</span>\n                </>\n              ) : null}\n              .\n            </p>\n          </div>\n        )}\n\n        <p className=\"mt-4 text-[11px] font-semibold tracking-wider text-muted-foreground uppercase\">\n          Evidence this code requires\n        </p>\n        <ul className=\"mt-1.5 flex flex-col gap-1.5\">\n          {evidence.map((item) => (\n            <li key={item} className=\"flex items-start gap-2 text-xs text-foreground\">\n              <CheckCircle\n                aria-hidden\n                className=\"mt-0.5 size-3.5 shrink-0 text-utility-green-600\"\n              />\n              {item}\n            </li>\n          ))}\n        </ul>\n      </div>\n      <div className=\"border-t border-border p-3\">\n        <Button className=\"w-full\" onClick={onCommit} data-slot=\"ewc-lookup-use-detail\">\n          Use {entry.isHazardous ? `${entry.code}*` : entry.code}\n        </Button>\n      </div>\n    </>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/blocks/domain/ewc-selector/ewc-lookup-modal.tsx"
    },
    {
      "path": "components/blocks/domain/ewc-selector/ewc-result-row.tsx",
      "content": "\"use client\"\n\nimport { cn } from \"@/lib/utils\"\n\nimport type { EwcEntry } from \"./types\"\n\ninterface EwcResultRowProps {\n  entry: EwcEntry\n  isHighlighted: boolean\n  onSelect: (entry: EwcEntry) => void\n}\n\n/** A single selectable leaf-code row inside the results listbox. */\nexport function EwcResultRow({ entry, isHighlighted, onSelect }: EwcResultRowProps) {\n  return (\n    <button\n      aria-selected={isHighlighted}\n      className={cn(\n        \"flex w-full items-center gap-3 rounded-md px-3 py-2.5 text-left transition-colors\",\n        isHighlighted ? \"bg-accent text-accent-foreground\" : \"hover:bg-muted\"\n      )}\n      data-code={entry.code}\n      id={`ewc-opt-${entry.code.replace(/\\s/g, \"-\")}`}\n      onClick={() => onSelect(entry)}\n      role=\"option\"\n      type=\"button\"\n    >\n      <span className=\"shrink-0 font-mono text-sm font-semibold tabular-nums\">\n        {entry.code}\n      </span>\n      <span className=\"min-w-0 flex-1 truncate text-sm text-muted-foreground\">\n        {entry.description}\n      </span>\n      {entry.isHazardous && <HazardousBadge />}\n    </button>\n  )\n}\n\n/** The small red \"Hazardous\" pill shown against hazardous codes. */\nexport function HazardousBadge({ className }: { className?: string }) {\n  return (\n    <span\n      className={cn(\n        \"shrink-0 rounded-md bg-destructive/10 px-2 py-0.5 text-xs font-medium text-destructive\",\n        className\n      )}\n    >\n      Hazardous\n    </span>\n  )\n}\n\n/** A non-selectable chapter/subcategory row acting as a group header. */\nexport function EwcGroupHeader({ entry }: { entry: EwcEntry }) {\n  return (\n    <div\n      className={cn(\n        \"px-3 py-2 text-xs font-semibold tracking-wider text-muted-foreground uppercase\",\n        entry.level === \"subcategory\" && \"pl-5\"\n      )}\n    >\n      {entry.code} — {entry.description}\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/blocks/domain/ewc-selector/ewc-result-row.tsx"
    },
    {
      "path": "components/blocks/domain/ewc-selector/ewc-search-panel.tsx",
      "content": "\"use client\"\n\nimport type { KeyboardEvent, RefObject } from \"react\"\nimport { useCallback, useEffect, useRef } from \"react\"\nimport { SearchLg } from \"@untitledui/icons\"\n\nimport { cn } from \"@/lib/utils\"\n\nimport { EwcGroupHeader, EwcResultRow } from \"./ewc-result-row\"\nimport type { EwcEntry } from \"./types\"\n\ninterface EwcSearchPanelProps {\n  search: string\n  setSearch: (search: string) => void\n  constructionOnly: boolean\n  setConstructionOnly: (value: boolean) => void\n  materialFilter: string | null\n  setMaterialFilter: (filter: string | null) => void\n  highlightedIndex: number\n  setHighlightedIndex: (index: number | ((prev: number) => number)) => void\n  chapterEntry: EwcEntry | null\n  selectableEntries: EwcEntry[] | null\n  clickableEntries: EwcEntry[]\n  leafCount: number\n  availableMaterials: string[]\n  defaultEntries: EwcEntry[]\n  defaultClickable: EwcEntry[]\n  recentCodes: EwcEntry[]\n  listboxId: string\n  searchRef: RefObject<HTMLInputElement | null>\n  activeEntries: EwcEntry[]\n  onSelect: (entry: EwcEntry) => void\n  onClose: () => void\n}\n\n/** The dropdown: search bar + C&D toggle, chapter strip, material pills, listbox. */\nexport function EwcSearchPanel({\n  search,\n  setSearch,\n  constructionOnly,\n  setConstructionOnly,\n  materialFilter,\n  setMaterialFilter,\n  highlightedIndex,\n  setHighlightedIndex,\n  chapterEntry,\n  selectableEntries,\n  clickableEntries,\n  leafCount,\n  availableMaterials,\n  defaultEntries,\n  defaultClickable,\n  recentCodes,\n  listboxId,\n  searchRef,\n  activeEntries,\n  onSelect,\n  onClose,\n}: EwcSearchPanelProps) {\n  const listRef = useRef<HTMLDivElement>(null)\n\n  // Keep the highlighted row scrolled into view under keyboard navigation.\n  useEffect(() => {\n    if (highlightedIndex < 0 || !listRef.current) return\n    const entries = selectableEntries === null ? defaultClickable : clickableEntries\n    const highlighted = entries[highlightedIndex]\n    if (!highlighted) return\n    const el = listRef.current.querySelector(`[data-code=\"${highlighted.code}\"]`)\n    if (el) el.scrollIntoView({ block: \"nearest\" })\n  }, [highlightedIndex, clickableEntries, defaultClickable, selectableEntries])\n\n  const handleSearchKeyDown = useCallback(\n    (e: KeyboardEvent<HTMLInputElement>) => {\n      if (e.key === \"Escape\") {\n        onClose()\n      } else if (e.key === \"ArrowDown\") {\n        e.preventDefault()\n        if (activeEntries.length > 0) {\n          setHighlightedIndex((prev) =>\n            prev < activeEntries.length - 1 ? prev + 1 : prev\n          )\n        }\n      } else if (e.key === \"ArrowUp\") {\n        e.preventDefault()\n        if (activeEntries.length > 0) {\n          setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : 0))\n        }\n      } else if (e.key === \"Enter\") {\n        e.preventDefault()\n        if (highlightedIndex >= 0 && activeEntries[highlightedIndex]) {\n          onSelect(activeEntries[highlightedIndex])\n        }\n      }\n    },\n    [activeEntries, highlightedIndex, onSelect, onClose, setHighlightedIndex]\n  )\n\n  return (\n    <div className=\"absolute inset-x-0 z-50 mt-2 flex max-h-[min(420px,60vh)] flex-col overflow-hidden rounded-lg border border-border bg-popover shadow-xl\">\n      {/* Search bar + C&D toggle */}\n      <div className=\"flex items-center gap-2 border-b border-border bg-muted/30 px-3 py-2.5\">\n        <SearchLg aria-hidden className=\"size-4 shrink-0 text-muted-foreground\" />\n        <input\n          className=\"flex-1 bg-transparent text-sm text-foreground outline-none placeholder:text-muted-foreground\"\n          onChange={(e) => {\n            setSearch(e.target.value)\n            setHighlightedIndex(-1)\n          }}\n          onKeyDown={handleSearchKeyDown}\n          placeholder=\"Search by code or description...\"\n          ref={searchRef}\n          type=\"text\"\n          value={search}\n        />\n        <button\n          aria-pressed={constructionOnly}\n          className={cn(\n            \"shrink-0 rounded-full border px-2.5 py-1 text-xs font-medium transition-colors\",\n            constructionOnly\n              ? \"border-foreground/20 bg-foreground/10 text-foreground\"\n              : \"border-transparent bg-muted text-muted-foreground hover:text-foreground\"\n          )}\n          onClick={() => {\n            setConstructionOnly(!constructionOnly)\n            setHighlightedIndex(-1)\n            setMaterialFilter(null)\n          }}\n          type=\"button\"\n        >\n          C&amp;D only\n        </button>\n      </div>\n\n      {/* Chapter context strip */}\n      {chapterEntry && selectableEntries !== null && (\n        <div className=\"border-b border-border bg-muted/20 px-3 py-1.5\">\n          <p className=\"text-xs text-muted-foreground\">\n            <span className=\"font-semibold\">{chapterEntry.code}</span>\n            {\" · \"}\n            <span className=\"capitalize\">\n              {chapterEntry.description.toLowerCase()}\n            </span>\n          </p>\n        </div>\n      )}\n\n      {/* Material filter pills */}\n      {availableMaterials.length > 1 && (\n        <div className=\"flex gap-1.5 overflow-x-auto border-b border-border px-3 py-2\">\n          {materialFilter && (\n            <button\n              className=\"shrink-0 rounded-full border border-border bg-background px-2.5 py-0.5 text-xs text-muted-foreground transition-colors hover:bg-muted\"\n              onClick={() => {\n                setMaterialFilter(null)\n                setHighlightedIndex(-1)\n              }}\n              type=\"button\"\n            >\n              ✕ All materials\n            </button>\n          )}\n          {availableMaterials.map((mat) => (\n            <button\n              className={cn(\n                \"shrink-0 rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors\",\n                materialFilter === mat\n                  ? \"bg-primary text-primary-foreground\"\n                  : \"border border-border bg-background text-muted-foreground hover:bg-muted\"\n              )}\n              key={mat}\n              onClick={() => {\n                setMaterialFilter(materialFilter === mat ? null : mat)\n                setHighlightedIndex(-1)\n              }}\n              type=\"button\"\n            >\n              {mat}\n            </button>\n          ))}\n        </div>\n      )}\n\n      {/* Listbox */}\n      <div\n        className=\"min-h-0 flex-1 overflow-y-auto p-2\"\n        id={listboxId}\n        ref={listRef}\n        role=\"listbox\"\n      >\n        {selectableEntries === null && (\n          <div className=\"space-y-1\">\n            {recentCodes.length > 0 && (\n              <>\n                <p className=\"px-3 pt-2 pb-1 text-xs font-semibold tracking-wider text-muted-foreground uppercase\">\n                  Recently used\n                </p>\n                {recentCodes.map((entry, i) => (\n                  <EwcResultRow\n                    entry={entry}\n                    isHighlighted={highlightedIndex === i}\n                    key={entry.code}\n                    onSelect={onSelect}\n                  />\n                ))}\n                <div className=\"my-1 border-t border-border\" />\n              </>\n            )}\n            <p className=\"px-3 pt-2 pb-1 text-xs font-semibold tracking-wider text-muted-foreground uppercase\">\n              {constructionOnly\n                ? \"Construction & demolition codes\"\n                : \"Common construction codes\"}\n            </p>\n            {defaultEntries.map((entry) => {\n              if (entry.level !== \"code\") {\n                return <EwcGroupHeader entry={entry} key={entry.code} />\n              }\n              const clickIdx = recentCodes.length + defaultClickable.indexOf(entry)\n              return (\n                <EwcResultRow\n                  entry={entry}\n                  isHighlighted={clickIdx === highlightedIndex}\n                  key={entry.code}\n                  onSelect={onSelect}\n                />\n              )\n            })}\n          </div>\n        )}\n\n        {selectableEntries !== null && selectableEntries.length === 0 && (\n          <div className=\"flex flex-col items-center justify-center gap-2 py-12 text-center\">\n            <p className=\"text-sm font-medium text-muted-foreground\">\n              No matching codes\n            </p>\n            <p className=\"text-xs text-muted-foreground/80\">\n              Try a different search or code prefix\n            </p>\n          </div>\n        )}\n\n        {selectableEntries !== null && selectableEntries.length > 0 && (\n          <div className=\"space-y-0.5\">\n            <p className=\"px-3 pt-1 pb-1 text-xs text-muted-foreground\">\n              {leafCount} {leafCount === 1 ? \"code\" : \"codes\"}\n            </p>\n            {selectableEntries.map((entry) => {\n              if (entry.level !== \"code\") {\n                return <EwcGroupHeader entry={entry} key={entry.code} />\n              }\n              const clickIdx = clickableEntries.indexOf(entry)\n              return (\n                <EwcResultRow\n                  entry={entry}\n                  isHighlighted={clickIdx === highlightedIndex}\n                  key={entry.code}\n                  onSelect={onSelect}\n                />\n              )\n            })}\n          </div>\n        )}\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/blocks/domain/ewc-selector/ewc-search-panel.tsx"
    },
    {
      "path": "components/blocks/domain/ewc-selector/ewc-segment-input.tsx",
      "content": "\"use client\"\n\nimport type { KeyboardEvent } from \"react\"\nimport { Hash02 } from \"@untitledui/icons\"\n\nimport { cn } from \"@/lib/utils\"\n\nimport type { EwcEntry } from \"./types\"\n\nconst SEGMENT_INPUT_CLASS =\n  \"w-8 bg-transparent text-center font-mono text-sm font-medium outline-none placeholder:text-muted-foreground/50 disabled:cursor-not-allowed\"\n\nconst SEGMENT_LABELS = [\n  \"EWC code first segment\",\n  \"EWC code second segment\",\n  \"EWC code third segment\",\n]\n\ninterface EwcSegmentInputProps {\n  index: number\n  value: string\n  disabled: boolean\n  open: boolean\n  highlightedIndex: number\n  activeEntries: EwcEntry[]\n  listboxId: string\n  segmentRef: (el: HTMLInputElement | null) => void\n  onChange: (index: number, value: string) => void\n  onFocus: () => void\n  onKeyDown: (index: number, e: KeyboardEvent<HTMLInputElement>) => void\n  onPaste: (e: React.ClipboardEvent<HTMLInputElement>) => void\n}\n\n/** One two-digit segment of the `00 · 00 · 00` code entry, ARIA-wired as a combobox. */\nexport function EwcSegmentInput({\n  index,\n  value,\n  disabled,\n  open,\n  highlightedIndex,\n  activeEntries,\n  listboxId,\n  segmentRef,\n  onChange,\n  onFocus,\n  onKeyDown,\n  onPaste,\n}: EwcSegmentInputProps) {\n  const highlighted = activeEntries[highlightedIndex]\n  return (\n    <input\n      aria-activedescendant={\n        open && highlightedIndex >= 0 && highlighted\n          ? `ewc-opt-${highlighted.code.replace(/\\s/g, \"-\")}`\n          : undefined\n      }\n      aria-autocomplete=\"list\"\n      aria-controls={open ? listboxId : undefined}\n      aria-label={SEGMENT_LABELS[index]}\n      className={SEGMENT_INPUT_CLASS}\n      disabled={disabled}\n      inputMode=\"numeric\"\n      maxLength={2}\n      onChange={(e) => onChange(index, e.target.value)}\n      onFocus={onFocus}\n      onKeyDown={(e) => onKeyDown(index, e)}\n      onPaste={onPaste}\n      placeholder=\"00\"\n      ref={segmentRef}\n      type=\"text\"\n      value={value}\n    />\n  )\n}\n\n/** The `·` separator between segments. */\nexport function SegmentSeparator({ className }: { className?: string }) {\n  return (\n    <span\n      aria-hidden\n      className={cn(\n        \"px-0.5 py-2 font-medium text-muted-foreground/60 select-none\",\n        className\n      )}\n    >\n      ·\n    </span>\n  )\n}\n\n/** The leading hash icon marking the field as a code/classification entry. */\nexport function EwcIcon() {\n  return <Hash02 aria-hidden className=\"size-4 shrink-0 text-muted-foreground\" />\n}\n",
      "type": "registry:component",
      "target": "components/blocks/domain/ewc-selector/ewc-segment-input.tsx"
    },
    {
      "path": "components/blocks/domain/ewc-selector/ewc-selector.tsx",
      "content": "\"use client\"\n\nimport { useCallback, useEffect, useRef, useState } from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nimport { EwcInputBar } from \"./ewc-input-bar\"\nimport { HazardousBadge } from \"./ewc-result-row\"\nimport { EwcSearchPanel } from \"./ewc-search-panel\"\nimport { loadRecentCodes, saveRecentCode } from \"./helpers\"\nimport type { EwcEntry, EwcSelectorProps } from \"./types\"\nimport { useEwcSearch } from \"./use-ewc-search\"\nimport { useEwcSegments } from \"./use-ewc-segments\"\n\n/**\n * EwcSelector — the flagship European Waste Catalogue picker.\n *\n * A hybrid of a segmented `00 · 00 · 00` code entry and a search-by-description\n * dropdown: type the code directly (auto-advancing, auto-selecting on a complete\n * match), paste six digits at once, or start typing a description to search.\n * Recently-used codes persist to localStorage (max 5); a \"C&D only\" toggle and\n * material-filter pills narrow the list; hazardous codes are flagged with a red\n * asterisk and a \"Hazardous\" badge.\n *\n * Controlled: pass `value` (e.g. \"17 06 01\") and handle `onChange`, which fires\n * with the resolved `EwcEntry` or `null` when cleared or incomplete.\n */\nexport function EwcSelector({\n  value,\n  onChange,\n  disabled = false,\n  className,\n}: EwcSelectorProps) {\n  const [open, setOpen] = useState(false)\n  const [search, setSearch] = useState(\"\")\n  const [highlightedIndex, setHighlightedIndex] = useState(-1)\n  const [constructionOnly, setConstructionOnly] = useState(false)\n  const [materialFilter, setMaterialFilter] = useState<string | null>(null)\n  const [recentCodes, setRecentCodes] = useState<EwcEntry[]>([])\n\n  const containerRef = useRef<HTMLDivElement>(null)\n  const searchRef = useRef<HTMLInputElement>(null)\n  const listboxId = useRef(`ewc-listbox-${Math.random().toString(36).slice(2)}`)\n\n  useEffect(() => setRecentCodes(loadRecentCodes()), [])\n\n  useEffect(() => {\n    const handler = (e: MouseEvent) => {\n      if (containerRef.current && !containerRef.current.contains(e.target as Node)) {\n        setOpen(false)\n      }\n    }\n    document.addEventListener(\"mousedown\", handler)\n    return () => document.removeEventListener(\"mousedown\", handler)\n  }, [])\n\n  const handleSelectRef = useRef<(entry: EwcEntry) => void>(() => {})\n  const handleSelectStable = useCallback(\n    (entry: EwcEntry) => handleSelectRef.current(entry),\n    []\n  )\n\n  const activeEntriesRef = useRef<EwcEntry[]>([])\n\n  const segmentApi = useEwcSegments({\n    value,\n    onChange,\n    open,\n    setOpen,\n    setSearch,\n    setHighlightedIndex,\n    setMaterialFilter,\n    searchRef,\n    activeEntries: activeEntriesRef.current,\n    highlightedIndex,\n    handleSelect: handleSelectStable,\n  })\n\n  const searchResults = useEwcSearch(\n    segmentApi.segments,\n    search,\n    constructionOnly,\n    materialFilter\n  )\n\n  const activeEntries =\n    searchResults.selectableEntries === null\n      ? searchResults.defaultClickable\n      : searchResults.clickableEntries\n  activeEntriesRef.current = activeEntries\n\n  handleSelectRef.current = (entry: EwcEntry) => {\n    const parts = entry.code.split(\" \")\n    segmentApi.setSegments([parts[0] ?? \"\", parts[1] ?? \"\", parts[2] ?? \"\"])\n    setSearch(\"\")\n    setOpen(false)\n    setHighlightedIndex(-1)\n    saveRecentCode(entry)\n    setRecentCodes(loadRecentCodes())\n    onChange(entry)\n  }\n\n  const handleClear = useCallback(() => {\n    segmentApi.setSegments([\"\", \"\", \"\"])\n    setSearch(\"\")\n    setOpen(false)\n    setHighlightedIndex(-1)\n    onChange(null)\n  }, [onChange, segmentApi.setSegments])\n\n  const selectedEntry = segmentApi.selectedEntry\n\n  return (\n    <div className={cn(\"relative\", className)} ref={containerRef}>\n      <EwcInputBar\n        activeEntries={activeEntries}\n        disabled={disabled}\n        hasValue={segmentApi.hasValue}\n        highlightedIndex={highlightedIndex}\n        listboxId={listboxId.current}\n        onClear={handleClear}\n        onFieldsetMouseDown={(e) => {\n          if (disabled) return\n          const tag = (e.target as HTMLElement).tagName\n          if (![\"INPUT\", \"BUTTON\", \"SVG\", \"PATH\", \"LINE\"].includes(tag)) {\n            e.preventDefault()\n            const firstEmpty = segmentApi.segments.findIndex((s) => s.length < 2)\n            segmentApi.segmentRefs.current[firstEmpty >= 0 ? firstEmpty : 0]?.focus()\n          }\n        }}\n        onPaste={segmentApi.handlePaste}\n        onSegmentChange={segmentApi.handleSegmentChange}\n        onSegmentFocus={() => {\n          if (!disabled) setOpen(true)\n        }}\n        onSegmentKeyDown={segmentApi.handleSegmentKeyDown}\n        open={open}\n        segmentRefs={segmentApi.segmentRefs}\n        segments={segmentApi.segments}\n        selectedEntry={selectedEntry}\n      />\n\n      {selectedEntry && !open && (\n        <div className=\"mt-1.5 flex items-center gap-2 rounded-md bg-muted/60 px-2 py-1\">\n          <p className=\"min-w-0 truncate text-xs font-medium text-foreground/80\">\n            {selectedEntry.description}\n          </p>\n          {selectedEntry.isHazardous && <HazardousBadge />}\n        </div>\n      )}\n\n      {segmentApi.codeNotFound && (\n        <p className=\"mt-1.5 text-sm text-destructive\">Code not found</p>\n      )}\n\n      {open && !disabled && (\n        <EwcSearchPanel\n          activeEntries={activeEntries}\n          availableMaterials={searchResults.availableMaterials}\n          chapterEntry={searchResults.chapterEntry}\n          clickableEntries={searchResults.clickableEntries}\n          constructionOnly={constructionOnly}\n          defaultClickable={searchResults.defaultClickable}\n          defaultEntries={searchResults.defaultEntries}\n          highlightedIndex={highlightedIndex}\n          leafCount={searchResults.leafCount}\n          listboxId={listboxId.current}\n          materialFilter={materialFilter}\n          onClose={() => setOpen(false)}\n          onSelect={handleSelectStable}\n          recentCodes={recentCodes}\n          search={search}\n          searchRef={searchRef}\n          selectableEntries={searchResults.selectableEntries}\n          setConstructionOnly={setConstructionOnly}\n          setHighlightedIndex={setHighlightedIndex}\n          setMaterialFilter={setMaterialFilter}\n          setSearch={setSearch}\n        />\n      )}\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/blocks/domain/ewc-selector/ewc-selector.tsx"
    },
    {
      "path": "components/blocks/domain/ewc-selector/helpers.ts",
      "content": "import { EWC_CODES } from \"./data\"\nimport type { EwcEntry, Segments } from \"./types\"\n\nexport const RECENTLY_USED_KEY = \"ewc-selector:recently-used\"\nexport const MAX_RECENT = 5\n\n// ─── Pure helpers ────────────────────────────────────────────────────────────\n\nexport function parseValueToSegments(value: string | null): Segments {\n  if (!value) return [\"\", \"\", \"\"]\n  const parts = value.split(\" \")\n  return [parts[0] ?? \"\", parts[1] ?? \"\", parts[2] ?? \"\"]\n}\n\nexport function segmentsToPrefix(segments: Segments): string {\n  return segments.filter((s) => s.length > 0).join(\" \")\n}\n\nexport function segmentsToCode(segments: Segments): string {\n  return `${segments[0]} ${segments[1]} ${segments[2]}`\n}\n\n/** Resolve three 2-digit segments to a leaf code entry, or `null`. */\nexport function lookupCode(segments: Segments): EwcEntry | null {\n  if (\n    segments[0].length !== 2 ||\n    segments[1].length !== 2 ||\n    segments[2].length !== 2\n  ) {\n    return null\n  }\n  const code = segmentsToCode(segments)\n  return EWC_CODES.find((e) => e.code === code && e.level === \"code\") ?? null\n}\n\n/**\n * Look up a full EWC code string and return the leaf entry, normalising common\n * separators — \"17.06.01\", \"17-06-01\" and \"17·06·01\" all resolve, and a\n * trailing hazardous asterisk (\"17 06 01*\") is stripped.\n */\nexport function findEwcEntry(code: string | null | undefined): EwcEntry | null {\n  if (!code) return null\n  const normalised = code\n    .replace(/[.\\-·]/g, \" \")\n    .replace(/\\*/g, \"\")\n    .replace(/\\s+/g, \" \")\n    .trim()\n  return EWC_CODES.find((e) => e.code === normalised && e.level === \"code\") ?? null\n}\n\n/** Derive the chapter entry from the first two digits of a code string. */\nexport function getChapterEntry(prefix: string): EwcEntry | null {\n  const chapterCode = prefix.trim().slice(0, 2)\n  if (chapterCode.length !== 2) return null\n  return (\n    EWC_CODES.find((e) => e.code === chapterCode && e.level === \"chapter\") ?? null\n  )\n}\n\n// ─── localStorage helpers ────────────────────────────────────────────────────\n\nexport function loadRecentCodes(): EwcEntry[] {\n  try {\n    const raw = localStorage.getItem(RECENTLY_USED_KEY)\n    if (!raw) return []\n    return JSON.parse(raw) as EwcEntry[]\n  } catch {\n    return []\n  }\n}\n\nexport function saveRecentCode(entry: EwcEntry): void {\n  try {\n    const prev = loadRecentCodes().filter((e) => e.code !== entry.code)\n    const next = [entry, ...prev].slice(0, MAX_RECENT)\n    localStorage.setItem(RECENTLY_USED_KEY, JSON.stringify(next))\n  } catch {\n    // ignore — SSR or storage-restricted environments\n  }\n}\n",
      "type": "registry:component",
      "target": "components/blocks/domain/ewc-selector/helpers.ts"
    },
    {
      "path": "components/blocks/domain/ewc-selector/index.ts",
      "content": "export { EwcSelector } from \"./ewc-selector\"\nexport { EwcCodeSummary } from \"./ewc-code-summary\"\nexport { EwcCodeDetail } from \"./ewc-code-detail\"\nexport { EwcLookupField, type EwcLookupFieldProps } from \"./ewc-lookup-field\"\nexport { EwcLookupModal, type EwcLookupModalProps } from \"./ewc-lookup-modal\"\nexport {\n  classifyEwc,\n  evidenceForEntry,\n  isMirrorEntry,\n  matchesEwcFilter,\n  mirrorPairCodes,\n  EWC_CHAPTERS,\n  type EwcChapterSummary,\n  type EwcHazardClass,\n  type EwcLookupFilter,\n} from \"./ewc-classify\"\nexport { findEwcEntry } from \"./helpers\"\nexport type { EwcCodeDetailProps, EwcEntry, EwcSelectorProps } from \"./types\"\n",
      "type": "registry:component",
      "target": "components/blocks/domain/ewc-selector/index.ts"
    },
    {
      "path": "components/blocks/domain/ewc-selector/types.ts",
      "content": "/**\n * EWC (European Waste Catalogue) selector — shared types.\n *\n * An `EwcEntry` mirrors one row of the flattened List of Waste (LoW,\n * 2014/955/EU) tree: 20 chapters, their subcategories, and the leaf codes,\n * plus the Nexus-added `material`, `isHazardous` and `isConstructionRelevant`\n * fields carried on the leaf codes.\n */\nexport interface EwcEntry {\n  /** Space-separated pairs, e.g. \"17 06 01\" (leaf), \"17 06\" (subcat), \"17\" (chapter). */\n  code: string\n  /** Chapters are UPPERCASE; subcategories and codes are lowercase. */\n  description: string\n  /** Material category — present on leaf codes only; `null` on chapters/subcats. */\n  material: string | null\n  isHazardous: boolean\n  level: \"chapter\" | \"subcategory\" | \"code\"\n  /** Flagged construction & demolition code (47 of the leaf codes). */\n  isConstructionRelevant: boolean\n}\n\nexport interface EwcSelectorProps {\n  /** The current full code, e.g. \"17 06 01\", or `null` when unset. */\n  value: string | null\n  /** Fires with the resolved entry, or `null` when cleared or incomplete. */\n  onChange: (entry: EwcEntry | null) => void\n  disabled?: boolean\n  className?: string\n}\n\nexport type Segments = [string, string, string]\n\nexport interface EwcCodeDetailProps {\n  entry: EwcEntry\n  className?: string\n}\n",
      "type": "registry:component",
      "target": "components/blocks/domain/ewc-selector/types.ts"
    },
    {
      "path": "components/blocks/domain/ewc-selector/use-ewc-search.ts",
      "content": "import { useMemo } from \"react\"\n\nimport { EWC_CODES } from \"./data\"\nimport { getChapterEntry, segmentsToPrefix } from \"./helpers\"\nimport type { Segments } from \"./types\"\n\n/**\n * Derives every search/filter value from the segments, the search text and the\n * active filters. Pure derived state — no internal state of its own.\n *\n * `selectableEntries === null` signals the default (no search, no prefix) view;\n * otherwise it is the filtered pool (chapters/subcats kept as group headers,\n * leaf codes as selectable rows).\n */\nexport function useEwcSearch(\n  segments: Segments,\n  search: string,\n  constructionOnly: boolean,\n  materialFilter: string | null\n) {\n  const prefix = useMemo(() => segmentsToPrefix(segments), [segments])\n\n  const chapterEntry = useMemo(\n    () => (prefix ? getChapterEntry(prefix) : null),\n    [prefix]\n  )\n\n  const selectableEntries = useMemo(() => {\n    const q = search.trim().toLowerCase()\n    if (!prefix && !q) return null\n\n    let pool = EWC_CODES\n\n    if (constructionOnly) {\n      pool = pool.filter((e) => e.isConstructionRelevant)\n    }\n\n    if (materialFilter) {\n      pool = pool.filter((e) => e.level !== \"code\" || e.material === materialFilter)\n    }\n\n    if (prefix) {\n      pool = pool.filter((e) => e.code.startsWith(prefix))\n    }\n\n    if (q) {\n      pool = pool.filter(\n        (e) =>\n          e.code.toLowerCase().includes(q) ||\n          e.description.toLowerCase().includes(q) ||\n          (e.material?.toLowerCase().includes(q) ?? false)\n      )\n    }\n\n    return pool\n  }, [search, constructionOnly, materialFilter, prefix])\n\n  const clickableEntries = useMemo(() => {\n    if (!selectableEntries) return []\n    return selectableEntries.filter((e) => e.level === \"code\")\n  }, [selectableEntries])\n\n  const leafCount = clickableEntries.length\n\n  const availableMaterials = useMemo(() => {\n    if (!selectableEntries || selectableEntries.length === 0) return []\n    const set = new Set<string>()\n    for (const e of selectableEntries) {\n      if (e.level === \"code\" && e.material) set.add(e.material)\n    }\n    return Array.from(set).sort()\n  }, [selectableEntries])\n\n  const defaultEntries = useMemo(() => {\n    let pool = constructionOnly\n      ? EWC_CODES.filter((e) => e.isConstructionRelevant)\n      : COMMON_CONSTRUCTION_ENTRIES\n    if (materialFilter) {\n      pool = pool.filter((e) => e.level !== \"code\" || e.material === materialFilter)\n    }\n    return pool\n  }, [constructionOnly, materialFilter])\n\n  const defaultClickable = useMemo(\n    () => defaultEntries.filter((e) => e.level === \"code\"),\n    [defaultEntries]\n  )\n\n  return {\n    prefix,\n    chapterEntry,\n    selectableEntries,\n    clickableEntries,\n    leafCount,\n    availableMaterials,\n    defaultEntries,\n    defaultClickable,\n  }\n}\n\n/**\n * The default (unfiltered) list shown before the user searches: chapter 17\n * (construction & demolition wastes) plus every construction-relevant code, so\n * the most common picks are one glance away.\n */\nconst COMMON_CONSTRUCTION_ENTRIES = EWC_CODES.filter(\n  (e) => e.code.startsWith(\"17\") || e.isConstructionRelevant\n)\n",
      "type": "registry:component",
      "target": "components/blocks/domain/ewc-selector/use-ewc-search.ts"
    },
    {
      "path": "components/blocks/domain/ewc-selector/use-ewc-segments.ts",
      "content": "import {\n  type KeyboardEvent,\n  type RefObject,\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\"\n\nimport { lookupCode, parseValueToSegments, saveRecentCode } from \"./helpers\"\nimport type { EwcEntry, Segments } from \"./types\"\n\ninterface UseEwcSegmentsOptions {\n  value: string | null\n  onChange: (entry: EwcEntry | null) => void\n  open: boolean\n  setOpen: (open: boolean) => void\n  setSearch: (search: string) => void\n  setHighlightedIndex: (index: number | ((prev: number) => number)) => void\n  setMaterialFilter: (filter: string | null) => void\n  searchRef: RefObject<HTMLInputElement | null>\n  activeEntries: EwcEntry[]\n  highlightedIndex: number\n  handleSelect: (entry: EwcEntry) => void\n}\n\n/**\n * Owns the segmented `00 · 00 · 00` code entry: auto-advance, paste, keyboard\n * navigation, and the auto-select-on-complete behaviour. Reads the latest\n * segments through a ref so keyboard handlers never see a stale closure.\n */\nexport function useEwcSegments({\n  value,\n  onChange,\n  open,\n  setOpen,\n  setSearch,\n  setHighlightedIndex,\n  setMaterialFilter,\n  searchRef,\n  activeEntries,\n  highlightedIndex,\n  handleSelect,\n}: UseEwcSegmentsOptions) {\n  const [segments, setSegments] = useState<Segments>(() =>\n    parseValueToSegments(value)\n  )\n  const segmentRefs = useRef<(HTMLInputElement | null)[]>([null, null, null])\n\n  const segmentsRef = useRef(segments)\n  segmentsRef.current = segments\n\n  const selectedEntry = useMemo(() => lookupCode(segments), [segments])\n\n  const hasValue = segments[0] !== \"\" || segments[1] !== \"\" || segments[2] !== \"\"\n\n  const codeNotFound =\n    segments[0].length === 2 &&\n    segments[1].length === 2 &&\n    segments[2].length === 2 &&\n    !selectedEntry\n\n  // Sync segments when the value prop changes externally.\n  useEffect(() => {\n    const next = parseValueToSegments(value)\n    setSegments((prev) => {\n      if (prev[0] === next[0] && prev[1] === next[1] && prev[2] === next[2]) {\n        return prev\n      }\n      return next\n    })\n  }, [value])\n\n  const handleSegmentChange = useCallback(\n    (index: number, val: string) => {\n      const digits = val.replace(/\\D/g, \"\").slice(0, 2)\n\n      setHighlightedIndex(-1)\n      setMaterialFilter(null)\n\n      setSegments((prev) => {\n        const next: Segments = [...prev]\n        next[index] = digits\n\n        if (digits.length === 2) {\n          const entry = lookupCode(next)\n          if (entry) {\n            // Auto-select on complete code: close, reset search, save recent.\n            setTimeout(() => {\n              saveRecentCode(entry)\n              onChange(entry)\n              setOpen(false)\n              setSearch(\"\")\n              setHighlightedIndex(-1)\n            }, 0)\n          } else {\n            setTimeout(() => onChange(null), 0)\n          }\n        } else {\n          setTimeout(() => onChange(null), 0)\n        }\n\n        return next\n      })\n\n      if (digits.length === 2 && index < 2) {\n        setTimeout(() => {\n          segmentRefs.current[index + 1]?.focus()\n          segmentRefs.current[index + 1]?.select()\n        }, 0)\n      }\n\n      if (!open) setOpen(true)\n    },\n    [open, onChange, setOpen, setSearch, setHighlightedIndex, setMaterialFilter]\n  )\n\n  const handlePaste = useCallback(\n    (e: React.ClipboardEvent<HTMLInputElement>) => {\n      const text = e.clipboardData.getData(\"text\").trim()\n      const digits = text.replace(/\\D/g, \"\")\n      if (digits.length >= 6) {\n        e.preventDefault()\n        const next: Segments = [\n          digits.slice(0, 2),\n          digits.slice(2, 4),\n          digits.slice(4, 6),\n        ]\n        setSegments(next)\n        setHighlightedIndex(-1)\n        setMaterialFilter(null)\n        if (!open) setOpen(true)\n        setTimeout(() => {\n          segmentRefs.current[2]?.focus()\n          const entry = lookupCode(next)\n          if (entry) {\n            saveRecentCode(entry)\n            onChange(entry)\n            setOpen(false)\n            setSearch(\"\")\n            setHighlightedIndex(-1)\n          }\n        }, 0)\n      }\n    },\n    [open, onChange, setOpen, setSearch, setHighlightedIndex, setMaterialFilter]\n  )\n\n  const handleSegmentKeyDown = useCallback(\n    (index: number, e: KeyboardEvent<HTMLInputElement>) => {\n      // A non-digit character redirects focus to the search box, seeded with\n      // that character — so users can just start typing a description.\n      if (\n        e.key.length === 1 &&\n        !/\\d/.test(e.key) &&\n        !e.ctrlKey &&\n        !e.metaKey &&\n        !e.altKey\n      ) {\n        e.preventDefault()\n        setSearch(e.key)\n        setHighlightedIndex(-1)\n        if (!open) setOpen(true)\n        setTimeout(() => searchRef.current?.focus(), 0)\n        return\n      }\n\n      const currentSegments = segmentsRef.current\n      if (e.key === \"Backspace\") {\n        if (currentSegments[index] === \"\" && index > 0) {\n          e.preventDefault()\n          segmentRefs.current[index - 1]?.focus()\n          segmentRefs.current[index - 1]?.select()\n        }\n      } else if (e.key === \"Escape\") {\n        setOpen(false)\n        ;(e.target as HTMLInputElement).blur()\n      } else if (e.key === \"ArrowDown\") {\n        e.preventDefault()\n        if (!open) {\n          setOpen(true)\n        } else if (activeEntries.length > 0) {\n          setHighlightedIndex((prev: number) =>\n            prev < activeEntries.length - 1 ? prev + 1 : prev\n          )\n        }\n      } else if (e.key === \"ArrowUp\") {\n        e.preventDefault()\n        if (activeEntries.length > 0) {\n          setHighlightedIndex((prev: number) => (prev > 0 ? prev - 1 : 0))\n        }\n      } else if (e.key === \"Enter\") {\n        e.preventDefault()\n        if (highlightedIndex >= 0 && activeEntries[highlightedIndex]) {\n          handleSelect(activeEntries[highlightedIndex])\n        }\n      }\n    },\n    [\n      open,\n      activeEntries,\n      highlightedIndex,\n      handleSelect,\n      setOpen,\n      setSearch,\n      setHighlightedIndex,\n      searchRef,\n    ]\n  )\n\n  return {\n    segments,\n    setSegments,\n    segmentRefs,\n    selectedEntry,\n    hasValue,\n    codeNotFound,\n    handleSegmentChange,\n    handlePaste,\n    handleSegmentKeyDown,\n  }\n}\n",
      "type": "registry:component",
      "target": "components/blocks/domain/ewc-selector/use-ewc-segments.ts"
    }
  ],
  "type": "registry:block"
}