{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "material-selection",
  "title": "Material Selection",
  "description": "The nx-ui Material Selection block.",
  "dependencies": [
    "@untitledui/icons@^0.0.23"
  ],
  "registryDependencies": [
    "@nx-ui/badge",
    "@nx-ui/button",
    "@nx-ui/card",
    "@nx-ui/collapsible",
    "@nx-ui/dialog",
    "@nx-ui/field",
    "@nx-ui/input",
    "@nx-ui/input-group",
    "@nx-ui/item",
    "@nx-ui/radio-group",
    "@nx-ui/select",
    "@nx-ui/separator",
    "@nx-ui/tabs",
    "@nx-ui/textarea",
    "@nx-ui/utils"
  ],
  "files": [
    {
      "path": "components/blocks/domain/material-selection/material-artwork.tsx",
      "content": "\"use client\"\n\nimport { useState } from \"react\"\nimport { LayersThree01 } from \"@untitledui/icons\"\nimport { cn } from \"@/lib/utils\"\nimport type { MaterialVisuals } from \"./material-selection-model\"\n\n/** Optional, host-supplied artwork. A failed image never prevents selection. */\nexport function MaterialArtwork({ classKey, visuals, className }: {\n  classKey: string; visuals?: MaterialVisuals; className?: string\n}) {\n  const url = visuals?.[classKey]?.url\n  const [failedUrl, setFailedUrl] = useState<string>()\n  return <div aria-hidden=\"true\" className={cn(\"flex shrink-0 items-center justify-center overflow-hidden rounded-lg bg-muted\", className)}>\n    {url && failedUrl !== url\n      ? <img src={url} alt=\"\" width={600} height={400} loading=\"lazy\" onError={() => setFailedUrl(url)} className=\"size-full object-cover\" />\n      : <LayersThree01 className=\"size-6 text-muted-foreground\" />}\n  </div>\n}\n",
      "type": "registry:component",
      "target": "components/blocks/domain/material-selection/material-artwork.tsx"
    },
    {
      "path": "components/blocks/domain/material-selection/material-browser.tsx",
      "content": "\"use client\"\n\nimport { useId, useState } from \"react\"\nimport { ArrowLeft, ArrowRight, SearchLg, XClose } from \"@untitledui/icons\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Button } from \"@/components/ui/button\"\nimport { Field, FieldDescription, FieldLabel } from \"@/components/ui/field\"\nimport { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from \"@/components/ui/input-group\"\nimport { Item, ItemContent, ItemDescription, ItemMedia, ItemTitle } from \"@/components/ui/item\"\nimport { Collapsible, CollapsibleContent, CollapsibleTrigger } from \"@/components/ui/collapsible\"\nimport { Separator } from \"@/components/ui/separator\"\nimport { Textarea } from \"@/components/ui/textarea\"\nimport { Tabs, TabsList, TabsTrigger } from \"@/components/ui/tabs\"\nimport { MaterialChoiceField } from \"./material-choice-field\"\nimport { MaterialArtwork } from \"./material-artwork\"\nimport { answerLabel, findMaterials, materialGroups, materialLabel, popularMaterials, specificationsFor, standardReference, normalise, type Origin, type Intent, type MaterialOption, type MaterialReference, type MaterialVisuals } from \"./material-selection-model\"\n\nexport function MaterialBrowser({ data, visuals, intent, onSelect, onSelectSpecification, onDescribe, compact = false }: {\n  data: MaterialReference; visuals?: MaterialVisuals; intent: Intent;\n  onSelect: (option: MaterialOption) => void; onSelectSpecification?: (key: string) => void; onDescribe: (description: string) => void; compact?: boolean\n}) {\n  const id = useId()\n  const [query, setQuery] = useState(\"\")\n  const [group, setGroup] = useState(\"\")\n  const [mode, setMode] = useState(\"material\")\n  const [origin, setOrigin] = useState<Origin>(\"\")\n  const [useKey, setUseKey] = useState(\"\")\n  const [description, setDescription] = useState(\"\")\n  const [describing, setDescribing] = useState(false)\n  const matches = findMaterials(data, query, query.trim() ? \"\" : group, { origin, useKey })\n  const uses = data.selectionReference?.uses ?? []\n  const use = uses.find(row => row.key === useKey)\n  const specifications = specificationsFor(data).filter(s => normalise(query).split(/\\s+/).every(word => normalise(`${s.name} ${s.key}`).includes(word)))\n  const groups = materialGroups(data)\n  const popularKeys = popularMaterials(data)\n  const visibleGroups = groups.filter(g => data.classes.some(c => c.isLeaf && g.classKeys.includes(c.key)))\n\n  return <div className=\"flex min-w-0 flex-col gap-6\">\n    {data.selectionReference && <Tabs value={mode} onValueChange={next => { setMode(next); setQuery(\"\"); setGroup(\"\"); setUseKey(\"\"); setOrigin(\"\") }}><TabsList aria-label=\"Browse catalogue\" className=\"w-full\"><TabsTrigger value=\"material\">By material</TabsTrigger><TabsTrigger value=\"use\">By use</TabsTrigger><TabsTrigger value=\"specification\">By specification</TabsTrigger></TabsList></Tabs>}\n    <Field>\n      <FieldLabel htmlFor={`${id}-search`}>{mode === \"specification\" ? \"Which specification do you know?\" : intent === \"need\" ? \"What material do you need?\" : \"What material do you have?\"}</FieldLabel>\n      <InputGroup className=\"h-11\">\n        <InputGroupAddon><SearchLg /></InputGroupAddon>\n        <InputGroupInput id={`${id}-search`} type=\"search\" value={query} onChange={e => setQuery(e.target.value)} placeholder=\"Topsoil, recycled Type 1, BS 3882…\" aria-describedby={`${id}-hint`} />\n        {query && <InputGroupAddon align=\"inline-end\"><InputGroupButton aria-label=\"Clear search\" onClick={() => setQuery(\"\")}><XClose /></InputGroupButton></InputGroupAddon>}\n      </InputGroup>\n      <FieldDescription id={`${id}-hint`}>Use a familiar name, a specification or a few details.</FieldDescription>\n    </Field>\n    {mode === \"specification\" ? <div className=\"flex max-h-112 flex-col gap-2 overflow-y-auto p-1 -m-1\">\n      <p className=\"pb-2 text-sm text-muted-foreground\">Start with the requirement. The material can remain undecided.</p>\n      {specifications.map(spec => { const ref = standardReference(data, spec.key); return <Item key={spec.key} variant=\"outline\" className=\"cursor-pointer text-left hover:bg-muted\" render={<button type=\"button\" onClick={() => onSelectSpecification?.(spec.key)} />}><ItemContent><ItemTitle className=\"whitespace-normal\">{spec.name}</ItemTitle><ItemDescription>{ref?.edition ?? \"Edition to confirm\"}{ref?.status === \"historical\" ? \" · historical reference\" : \"\"}</ItemDescription></ItemContent><ArrowRight className=\"size-4 shrink-0\" /></Item> })}\n      {!specifications.length && <p role=\"status\" className=\"text-sm text-muted-foreground\">No matching specifications. Try another designation.</p>}\n    </div> : mode === \"use\" && !useKey && !query.trim() ? <div className=\"grid gap-3 sm:grid-cols-2\">{uses.map(use => <Item key={use.key} variant=\"outline\" className=\"cursor-pointer text-left hover:bg-muted\" render={<button type=\"button\" onClick={() => setUseKey(use.key)} />}><ItemContent><ItemTitle>{use.label}</ItemTitle><ItemDescription>{use.description}</ItemDescription></ItemContent><ArrowRight className=\"size-4 shrink-0\" /></Item>)}</div> : <>\n    {(group === \"aggregates\" || query.trim() || useKey) && <MaterialChoiceField<Origin> label=\"Aggregate origin\" value={origin} onChange={setOrigin} options={[[\"\", \"All\"], [\"primary\", \"Primary\"], [\"recycled\", \"Recycled\"], [\"secondary\", \"Secondary\"], [\"blended\", \"Blended\"]]} />}\n    {use && <div className=\"space-y-2\"><Button variant=\"ghost\" size=\"sm\" onClick={() => setUseKey(\"\")}><ArrowLeft />{use.label}</Button><p className=\"text-sm text-muted-foreground\">{use.question} Explore these starting points; project suitability needs checking.</p></div>}\n    {!query.trim() && !group && !useKey ? <>\n      <div className=\"flex flex-wrap items-center gap-2\"><span className=\"mr-1 text-sm text-muted-foreground\">Common choices</span>{popularKeys.map(key => {\n        const material = data.classes.find(c => c.key === key)\n        return material && <Button key={key} variant=\"secondary\" size=\"sm\" onClick={() => onSelect({ material })}>{materialLabel(data, material).replace(\" Sub-base\", \"\").replace(\" Capping\", \"\")}</Button>\n      })}</div>\n      <div className=\"flex flex-col gap-3\"><h3 className=\"text-sm font-semibold\">Browse by material</h3>\n        <div className={compact ? \"grid gap-3\" : \"grid gap-3 sm:grid-cols-2\"}>{visibleGroups.map(g => <Item key={g.key} variant=\"outline\" className=\"cursor-pointer rounded-xl text-left hover:bg-muted\" render={<button type=\"button\" onClick={() => setGroup(g.key)} />}>\n          {g.visualClassKey && <ItemMedia><MaterialArtwork classKey={g.visualClassKey} visuals={visuals} className=\"size-14\" /></ItemMedia>}<ItemContent className=\"min-w-0\"><ItemTitle className=\"whitespace-normal font-semibold\">{g.label}</ItemTitle><ItemDescription>{g.detail}</ItemDescription></ItemContent><ArrowRight aria-hidden=\"true\" className=\"size-4 text-muted-foreground\" />\n        </Item>)}</div>\n      </div>\n    </> : <>\n      <div className=\"flex items-center justify-between gap-3\">{group && !query.trim() ? <Button variant=\"ghost\" size=\"sm\" onClick={() => setGroup(\"\")}><ArrowLeft />{groups.find(g => g.key === group)?.label}</Button> : <h3 className=\"text-sm font-semibold\">Matching materials</h3>}<span role=\"status\" className=\"text-sm text-muted-foreground\">{matches.length} {matches.length === 1 ? \"match\" : \"matches\"}</span></div>\n      <div className=\"flex max-h-112 flex-col gap-2 overflow-y-auto p-1 -m-1\">{matches.map(option => <Item key={option.material.key} variant=\"outline\" className=\"cursor-pointer rounded-xl text-left hover:bg-muted\" render={<button type=\"button\" aria-label={`Choose ${materialLabel(data, option.material)}`} onClick={() => onSelect(option)} />}>\n        <ItemMedia><MaterialArtwork classKey={option.material.key} visuals={visuals} className=\"size-12\" /></ItemMedia>\n        <ItemContent className=\"min-w-0\"><ItemTitle className=\"whitespace-normal font-semibold\">{materialLabel(data, option.material)}</ItemTitle><ItemDescription>{option.matchedName ?? groups.find(g => g.classKeys.includes(option.material.key))?.label}</ItemDescription>\n          <span className=\"flex flex-wrap gap-1.5\">{option.processing && <Badge variant=\"gray\">{answerLabel(option.processing, intent)}</Badge>}{option.origin && <Badge variant=\"gray\">{answerLabel(option.origin, intent)}</Badge>}{option.specification && <Badge variant=\"gray\">{option.specification === \"family:BS3882\" ? \"BS 3882 · grade to confirm\" : data.specTemplates.find(s => `template:${s.key}` === option.specification)?.name ?? \"Specification\"}</Badge>}</span>\n        </ItemContent><ArrowRight aria-hidden=\"true\" className=\"size-4 text-muted-foreground\" />\n      </Item>)}</div>\n      {!matches.length && <div className=\"flex flex-col items-start gap-3 rounded-xl border border-dashed p-6\"><h3 className=\"font-semibold\">No matching materials yet</h3><p className=\"text-sm text-muted-foreground\">Try a broader name, or keep your description and identify it later.</p><Button variant=\"secondary\" onClick={() => { setQuery(\"\"); setGroup(\"\") }}>Browse all materials</Button></div>}\n    </>}\n    </>}\n    <Separator />\n    <Collapsible open={describing} onOpenChange={open => { setDescribing(open); if (open && !description) setDescription(query) }}>\n      <CollapsibleTrigger render={<Button variant=\"link\" className=\"h-auto justify-start whitespace-normal p-0 text-left\" />}>Can’t find your material? Describe it instead <ArrowRight /></CollapsibleTrigger>\n      <CollapsibleContent><div className=\"flex flex-col items-start gap-4 pt-5\"><Field><FieldLabel htmlFor={`${id}-description`}>What do you know about it?</FieldLabel><Textarea id={`${id}-description`} value={description} onChange={e => setDescription(e.target.value)} placeholder=\"For example, mixed material from digging the foundations…\" /></Field><Button disabled={!description.trim()} onClick={() => onDescribe(description.trim())}>Use this description<ArrowRight /></Button></div></CollapsibleContent>\n    </Collapsible>\n  </div>\n}\n",
      "type": "registry:component",
      "target": "components/blocks/domain/material-selection/material-browser.tsx"
    },
    {
      "path": "components/blocks/domain/material-selection/material-choice-field.tsx",
      "content": "\"use client\"\n\nimport { useId } from \"react\"\nimport { FieldDescription, FieldLabel, FieldLegend, FieldSet } from \"@/components/ui/field\"\nimport { RadioGroup, RadioGroupItem } from \"@/components/ui/radio-group\"\n\nexport function MaterialChoiceField<T extends string>({ label, description, value, options, onChange }: {\n  label: string; description?: string; value: T; options: readonly (readonly [T, string])[]; onChange: (value: T) => void\n}) {\n  const id = useId()\n  return <FieldSet className=\"gap-3\"><FieldLegend variant=\"label\" id={`${id}-legend`} className=\"mb-0 font-semibold\">{label}</FieldLegend>{description && <FieldDescription id={`${id}-help`}>{description}</FieldDescription>}\n    <RadioGroup aria-labelledby={`${id}-legend`} aria-describedby={description ? `${id}-help` : undefined} value={value} onValueChange={next => { const selected = options.find(([key]) => key === next); if (selected) onChange(selected[0]) }} className=\"flex flex-wrap gap-x-5 gap-y-4\">\n      {options.map(([key, text]) => <div key={key} className=\"flex min-h-6 items-center gap-2\"><RadioGroupItem id={`${id}-${key}`} value={key} /><FieldLabel htmlFor={`${id}-${key}`} className=\"font-normal\">{text}</FieldLabel></div>)}\n    </RadioGroup>\n  </FieldSet>\n}\n",
      "type": "registry:component",
      "target": "components/blocks/domain/material-selection/material-choice-field.tsx"
    },
    {
      "path": "components/blocks/domain/material-selection/material-details.tsx",
      "content": "\"use client\"\n\nimport { useId } from \"react\"\nimport { ChevronDown, XClose } from \"@untitledui/icons\"\nimport { Button } from \"@/components/ui/button\"\nimport { Field, FieldDescription, FieldGroup, FieldLabel } from \"@/components/ui/field\"\nimport { Input } from \"@/components/ui/input\"\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ui/select\"\nimport { Textarea } from \"@/components/ui/textarea\"\nimport { Collapsible, CollapsibleContent, CollapsibleTrigger } from \"@/components/ui/collapsible\"\nimport { MaterialQuantity } from \"./material-quantity\"\nimport { MaterialChoiceField } from \"./material-choice-field\"\nimport { answerLabel, hasProcessing, descendantOf, originsFor, specificationsFor, standardReference, type MaterialDraft, type MaterialReference } from \"./material-selection-model\"\n\nexport function MaterialDetails({ data, value, onChange }: {\n  data: MaterialReference; value: MaterialDraft; onChange: (value: MaterialDraft) => void\n}) {\n  const id = useId()\n  const need = value.intent === \"need\"\n  const chosen = data.classes.find(c => c.key === value.material?.classKey)\n  const term = data.terms.find(t => t.code === value.material?.termCode)\n  const patch = (next: Partial<MaterialDraft>) => onChange({ ...value, ...next })\n  const specs = specificationsFor(data, chosen?.key)\n  const origins = chosen ? originsFor(data, chosen.key) : []\n  const use = data.selectionReference?.uses.find(row => row.key === value.useKey)\n  const reference = value.specification.startsWith(\"template:\") ? standardReference(data, value.specification.slice(9)) : undefined\n  const specOptions = [\n    { value: \"\", label: \"Choose if known…\" }, { value: \"unknown\", label: \"Not sure yet\" },\n    { value: \"none\", label: need ? \"No specified standard\" : \"None reported\" },\n    ...(chosen?.key === \"soil.topsoil\" ? [{ value: \"family:BS3882\", label: \"BS 3882 — grade to confirm\" }] : []),\n    ...specs.map(s => ({ value: `template:${s.key}`, label: s.name })),\n    { value: \"custom\", label: \"A project or custom specification\" },\n  ]\n  return <FieldGroup>\n    <div className=\"space-y-1\"><h3 className=\"font-semibold\">{need ? \"What matters for your project?\" : \"What do you know about it?\"}</h3><p className=\"text-sm text-muted-foreground\">{need ? \"Add your requirements. Leave the rest open.\" : \"Record the details you know. Uncertainty is fine.\"}</p></div>\n    {term && <div className=\"rounded-lg border bg-muted/40 p-3\"><div className=\"flex items-start justify-between gap-3\"><p className=\"text-sm\">Selected name: <span className=\"font-medium\">{term.label}</span></p><Button variant=\"ghost\" size=\"icon-sm\" aria-label=\"Clear selected designation\" onClick={() => patch({ material: value.material ? { classKey: value.material.classKey } : null })}><XClose /></Button></div><p className=\"text-xs text-muted-foreground\">The name is recorded separately from the standard below.</p></div>}\n    {chosen && hasProcessing(chosen.key) && <MaterialChoiceField<MaterialDraft[\"processing\"]> label={need ? \"Preparation required\" : \"Reported preparation\"} value={value.processing} onChange={processing => patch({ processing })} options={[\n      ...(descendantOf(data, chosen.key, \"soil\") ? [[\"as-dug\", \"As-dug\"] as const] : []), [\"screened\", \"Screened\"], [\"unscreened\", \"Unscreened\"], ...(need ? [[\"none\", \"No preference\"] as const] : []), [\"unknown\", \"Not sure yet\"],\n    ]} />}\n    {chosen && origins.length > 0 && <>\n      {origins.length === 1 ? <Field><FieldLabel>Origin</FieldLabel><FieldDescription>{answerLabel(value.origin, value.intent)} — part of the selected material name.</FieldDescription></Field> : <MaterialChoiceField<MaterialDraft[\"origin\"]> label={need ? \"Material origin\" : \"Reported origin\"} description=\"Origin and the material’s specification are separate.\" value={value.origin} onChange={origin => patch({ origin })} options={[[\"primary\", \"Primary\"], [\"secondary\", \"Secondary\"], [\"recycled\", \"Recycled\"], [\"blended\", \"Blended\"], ...(need ? [[\"none\", \"No preference\"] as const] : []), [\"unknown\", \"Not sure yet\"]]} />}\n      {need && origins.length !== 1 && ![\"\", \"unknown\", \"none\"].includes(value.origin) && <MaterialChoiceField<MaterialDraft[\"originStrength\"]> label=\"How important is this origin?\" value={value.originStrength} onChange={originStrength => patch({ originStrength })} options={[[\"required\", \"Required\"], [\"preferred\", \"Preferred; alternatives considered\"]]} />}\n    </>}\n    <Field><FieldLabel htmlFor={`${id}-standard`}>{need ? \"Required standard\" : \"Reported standard\"}</FieldLabel>\n      <Select items={specOptions} value={value.specification} onValueChange={next => { if (typeof next === \"string\") patch({ specification: next, customSpecification: \"\", additionalSpecifications: [\"\", \"unknown\", \"none\"].includes(next) ? [] : value.additionalSpecifications }) }}>\n        <SelectTrigger id={`${id}-standard`} className=\"w-full min-w-0\"><SelectValue /></SelectTrigger>\n        <SelectContent alignItemWithTrigger={false}>{specOptions.map(s => <SelectItem key={s.value} value={s.value} className=\"**:data-[slot=select-item-text]:whitespace-normal\">{s.label}</SelectItem>)}</SelectContent>\n      </Select>\n      <FieldDescription>{value.specification === \"family:BS3882\" ? \"BS 3882 is recorded. The grade is still to confirm.\" : need ? \"Choose a known requirement, or add your own specification.\" : \"A reported standard still needs supporting evidence.\"}</FieldDescription>\n    </Field>\n    {reference && <p className=\"text-xs text-muted-foreground\">{reference.edition ? `${reference.document} · ${reference.edition} · ${reference.jurisdictions.join(\", \")}` : \"Edition unconfirmed — check the project documents.\"}{reference.status === \"historical\" && \" · Historical reference; confirm the edition required by your contract.\"}</p>}\n    {value.specification && ![\"unknown\", \"none\"].includes(value.specification) && <Collapsible>\n      <CollapsibleTrigger render={<Button variant=\"link\" className=\"h-auto justify-start p-0\" />}>Additional standards or alternatives{value.additionalSpecifications.length ? ` (${value.additionalSpecifications.length})` : \"\"}</CollapsibleTrigger>\n      <CollapsibleContent className=\"space-y-4 pt-4\">\n        <MaterialChoiceField<MaterialDraft[\"specificationOperator\"]> label=\"How should the standards apply?\" value={value.specificationOperator} onChange={specificationOperator => patch({ specificationOperator })} options={[[\"all\", \"All are required\"], [\"any\", \"Any one is acceptable\"]]} />\n        {value.additionalSpecifications.map(key => <div key={key} className=\"flex items-center justify-between gap-3 text-sm\"><span>{data.specTemplates.find(s => `template:${s.key}` === key)?.name ?? key}</span><Button variant=\"ghost\" size=\"sm\" onClick={() => patch({ additionalSpecifications: value.additionalSpecifications.filter(s => s !== key) })}>Remove</Button></div>)}\n        <Field><FieldLabel htmlFor={`${id}-extra`}>Add another standard</FieldLabel><Select value=\"\" items={[{ value: \"\", label: \"Choose another…\" }, ...specs.filter(s => `template:${s.key}` !== value.specification && !value.additionalSpecifications.includes(`template:${s.key}`)).map(s => ({ value: `template:${s.key}`, label: s.name }))]} onValueChange={next => { if (next) patch({ additionalSpecifications: [...value.additionalSpecifications, next] }) }}><SelectTrigger id={`${id}-extra`} className=\"w-full\"><SelectValue /></SelectTrigger><SelectContent alignItemWithTrigger={false}><SelectItem value=\"\">Choose another…</SelectItem>{specs.filter(s => `template:${s.key}` !== value.specification && !value.additionalSpecifications.includes(`template:${s.key}`)).map(s => <SelectItem key={s.key} value={`template:${s.key}`}>{s.name}</SelectItem>)}</SelectContent></Select></Field>\n      </CollapsibleContent>\n    </Collapsible>}\n    {value.specification === \"custom\" && <Field><FieldLabel htmlFor={`${id}-custom`}>Your specification</FieldLabel><Textarea id={`${id}-custom`} value={value.customSpecification} onChange={e => patch({ customSpecification: e.target.value })} placeholder=\"Name the project specification, or describe what matters…\" /></Field>}\n    {!need && <MaterialChoiceField<MaterialDraft[\"evidence\"]> label=\"Supporting documents\" description=\"A soil report, test result or product sheet. These can be added later.\" value={value.evidence} onChange={evidence => patch({ evidence })} options={[[\"available\", \"Available\"], [\"none\", \"No documents yet\"], [\"unknown\", \"Not sure yet\"]]} />}\n    {use && <p className=\"text-sm text-muted-foreground\"><strong>{use.label}:</strong> {use.question} These are starting points; suitability still needs checking.</p>}\n    {data.selectionReference && <MaterialQuantity\n      units={data.selectionReference.quantityUnits}\n      densityReferences={data.selectionReference.densityReferences}\n      catalogueVersion={data.version}\n      classKey={chosen?.key}\n      value={value.quantity}\n      onChange={quantity => patch({ quantity })}\n    />}\n    {need && <Field><FieldLabel htmlFor={`${id}-use`}>Intended use <span className=\"font-normal text-muted-foreground\">(optional)</span></FieldLabel><Input id={`${id}-use`} value={value.purpose} onChange={e => patch({ purpose: e.target.value })} placeholder=\"For example, landscaping or an access road\" /></Field>}\n    <Collapsible><CollapsibleTrigger render={<Button variant=\"ghost\" className=\"justify-start px-0\" />}>Anything else to add?<ChevronDown /></CollapsibleTrigger><CollapsibleContent><Field className=\"pt-3\"><FieldLabel htmlFor={`${id}-notes`}>Additional details</FieldLabel><Textarea id={`${id}-notes`} value={value.notes} onChange={e => patch({ notes: e.target.value })} placeholder=\"Size, condition, exclusions or anything else to confirm…\" /></Field></CollapsibleContent></Collapsible>\n  </FieldGroup>\n}\n",
      "type": "registry:component",
      "target": "components/blocks/domain/material-selection/material-details.tsx"
    },
    {
      "path": "components/blocks/domain/material-selection/material-quantity-model.ts",
      "content": "/** Portable quantity contract. Definitions come from UMM; this module never guesses a density. */\nexport interface QuantityUnit {\n  id: string; kind: string; label: string; symbol: string; ucum: string;\n  factorToBase: number; baseUnit: string;\n}\ntype QuantityBaseUnit = \"m3\" | \"kg\" | \"m\" | \"m2\";\ntype StableQuantityUnit = Pick<QuantityUnit, \"kind\" | \"baseUnit\" | \"factorToBase\" | \"ucum\">;\nconst stableQuantityUnits: Readonly<Record<string, StableQuantityUnit>> = {\n  m3: { kind: \"volume\", baseUnit: \"m3\", factorToBase: 1, ucum: \"m3\" },\n  L: { kind: \"volume\", baseUnit: \"m3\", factorToBase: 0.001, ucum: \"L\" },\n  yd3: { kind: \"volume\", baseUnit: \"m3\", factorToBase: 0.764554857984, ucum: \"[cyd_i]\" },\n  ft3: { kind: \"volume\", baseUnit: \"m3\", factorToBase: 0.028316846592, ucum: \"[cft_i]\" },\n  t: { kind: \"mass\", baseUnit: \"kg\", factorToBase: 1000, ucum: \"t\" },\n  kg: { kind: \"mass\", baseUnit: \"kg\", factorToBase: 1, ucum: \"kg\" },\n  lb: { kind: \"mass\", baseUnit: \"kg\", factorToBase: 0.45359237, ucum: \"[lb_av]\" },\n  \"short-ton\": { kind: \"mass\", baseUnit: \"kg\", factorToBase: 907.18474, ucum: \"[ston_av]\" },\n  \"long-ton\": { kind: \"mass\", baseUnit: \"kg\", factorToBase: 1016.0469088, ucum: \"[lton_av]\" },\n  m: { kind: \"length\", baseUnit: \"m\", factorToBase: 1, ucum: \"m\" },\n  cm: { kind: \"length\", baseUnit: \"m\", factorToBase: 0.01, ucum: \"cm\" },\n  mm: { kind: \"length\", baseUnit: \"m\", factorToBase: 0.001, ucum: \"mm\" },\n  ft: { kind: \"length\", baseUnit: \"m\", factorToBase: 0.3048, ucum: \"[ft_i]\" },\n  in: { kind: \"length\", baseUnit: \"m\", factorToBase: 0.0254, ucum: \"[in_i]\" },\n  yd: { kind: \"length\", baseUnit: \"m\", factorToBase: 0.9144, ucum: \"[yd_i]\" },\n  m2: { kind: \"area\", baseUnit: \"m2\", factorToBase: 1, ucum: \"m2\" },\n  ft2: { kind: \"area\", baseUnit: \"m2\", factorToBase: 0.09290304, ucum: \"[ft_i]2\" },\n};\nconst quantityBaseKinds: Readonly<Record<QuantityBaseUnit, string>> = {\n  m3: \"volume\", kg: \"mass\", m: \"length\", m2: \"area\",\n};\nconst record = (value: unknown): value is Record<string, unknown> => typeof value === \"object\" && value !== null && !Array.isArray(value);\n\n/** Stable v1 IDs have immutable physical meanings; future IDs may extend a known base. */\nexport function isQuantityUnit(value: unknown): value is QuantityUnit {\n  if (!record(value) || typeof value.id !== \"string\" || typeof value.kind !== \"string\" || typeof value.label !== \"string\" || typeof value.symbol !== \"string\" || typeof value.ucum !== \"string\" || typeof value.baseUnit !== \"string\" || !value.id.trim() || !value.kind.trim() || !value.label.trim() || !value.symbol.trim() || !value.ucum.trim() || !value.baseUnit.trim() || typeof value.factorToBase !== \"number\" || !Number.isFinite(value.factorToBase) || value.factorToBase <= 0) return false;\n  const stable = stableQuantityUnits[value.id];\n  if (stable) return value.kind === stable.kind && value.baseUnit === stable.baseUnit && value.factorToBase === stable.factorToBase && value.ucum === stable.ucum;\n  return value.baseUnit in quantityBaseKinds && quantityBaseKinds[value.baseUnit as QuantityBaseUnit] === value.kind;\n}\nexport type VolumeBasis = \"unknown\" | \"in-situ\" | \"loose\" | \"compacted\";\nexport type MoistureBasis = \"unknown\" | \"as-received\" | \"dry\";\nexport interface DensityReference {\n  id: string; classKey: string; version: string; unit: string; basis: string;\n  moistureBasis: string; min: number; mid: number; max: number; source: string;\n  reviewStatus: string; usableForConversion: boolean; catalogueVersion?: string;\n  review?: { id: string; reviewedAt: string; finding: string; reason: string };\n}\nexport interface SpatialReference {\n  provider: string; resourceId: string; revision: string; schema: string;\n}\nexport interface QuantityDraft {\n  mode: \"\" | \"unknown\" | \"exact\" | \"measured\" | \"approximate\" | \"range\" | \"dimensions\" | \"area-depth\";\n  source: string; unit: string; value: string; min: string; max: string;\n  length: string; width: string; depth: string; area: string;\n  lengthUnit: string; depthUnit: string; areaUnit: string;\n  basis: VolumeBasis; moistureBasis: MoistureBasis;\n  targetUnit: string; density: string; densitySource: string;\n  densityBasis: VolumeBasis; densityMoistureBasis: MoistureBasis;\n  densityReferenceId: string;\n  spatialReference?: SpatialReference;\n}\nexport const blankQuantity = (unit = \"m3\"): QuantityDraft => ({\n  mode: \"\", source: \"\", unit, value: \"\", min: \"\", max: \"\", length: \"\", width: \"\", depth: \"\", area: \"\",\n  lengthUnit: \"m\", depthUnit: \"m\", areaUnit: \"m2\", basis: \"unknown\", moistureBasis: \"unknown\",\n  targetUnit: \"\", density: \"\", densitySource: \"\", densityBasis: \"unknown\", densityMoistureBasis: \"unknown\", densityReferenceId: \"\",\n});\nexport type DensityVerification = \"not-assessed\" | \"reviewed-reference\";\nexport interface DensityReferenceSelection {\n  id: string;\n  version: string;\n  catalogueVersion: string;\n}\nexport interface QuantityContext {\n  classKey?: string;\n  densityReferences?: readonly DensityReference[];\n  catalogueVersion?: string;\n}\nexport interface QuantityPayload {\n  state: \"unknown\" | \"specified\";\n  kind?: string; unitId?: string; value?: number; min?: number; max?: number;\n  basis?: VolumeBasis; moistureBasis?: MoistureBasis;\n  source?: string; method?: \"stated\" | \"measured\" | \"calculated\"; accuracy?: \"stated\" | \"approximate\";\n  derivation?: { method: \"box\" | \"area-depth\"; version: \"1\"; inputs: Record<string, { value: number; unitId: string }> };\n    conversion?: { state: \"available\" | \"unavailable\"; targetUnitId: string; reason?: string;\n    value?: number; min?: number; max?: number; estimated?: boolean;\n    density?: { value: number; unit: \"t/m3\"; basis: VolumeBasis; moistureBasis: MoistureBasis; source: string; verification: DensityVerification; reference?: DensityReferenceSelection } };\n  spatialReference?: SpatialReference;\n}\nconst number = (input: string): number | undefined => {\n  const text = input.trim();\n  const value = /^\\d{1,3}(,\\d{3})+(\\.\\d+)?$/.test(text) ? text.replaceAll(\",\", \"\") : text;\n  if (!/^(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:[eE][+-]?\\d+)?$/.test(value)) return undefined;\n  const n = Number(value); return Number.isFinite(n) && n > 0 ? n : undefined;\n};\n\n/**\n * A density can drive a conversion only when UMM has explicitly enabled it and\n * the record has passed review. `usableForConversion` is deliberately not\n * sufficient on its own: a producer must also publish a reviewed status.\n */\nexport const isReviewedDensityReference = (reference: DensityReference) =>\n  reference.usableForConversion &&\n  reference.reviewStatus === \"reviewed-usable\" &&\n  Boolean(reference.review && reference.review.id.trim() && reference.review.reviewedAt.trim() && reference.review.finding.trim() && reference.review.reason.trim()) &&\n  reference.unit === \"t/m3\" &&\n  reference.min > 0 && reference.min <= reference.mid && reference.mid <= reference.max &&\n  Boolean(reference.source.trim());\n\nexport function compatibleDensityReferences(\n  references: readonly DensityReference[] | undefined,\n  classKey: string | undefined,\n  basis: VolumeBasis,\n  moistureBasis: MoistureBasis,\n  catalogueVersion?: string,\n) {\n  if (!references || !classKey || basis === \"unknown\" || moistureBasis === \"unknown\") return [];\n  return references.filter(reference =>\n    reference.classKey === classKey &&\n    reference.basis === basis &&\n    reference.moistureBasis === moistureBasis &&\n    (!reference.catalogueVersion || !catalogueVersion || reference.catalogueVersion === catalogueVersion) &&\n    isReviewedDensityReference(reference),\n  );\n}\n\nexport function quantityResult(draft: QuantityDraft | undefined, units: readonly QuantityUnit[], context: QuantityContext = {}): { quantity: QuantityPayload | null; errors: string[] } {\n  if (!draft?.mode) return { quantity: null, errors: [] };\n  if (draft.mode === \"unknown\") return { quantity: { state: \"unknown\", ...(draft.spatialReference ? { spatialReference: draft.spatialReference } : {}) }, errors: [] };\n  const errors: string[] = [];\n  const getUnit = (id: string, kind?: string) => units.find(u => isQuantityUnit(u) && u.id === id && (!kind || u.kind === kind));\n  const geometry = draft.mode === \"dimensions\" || draft.mode === \"area-depth\";\n  const unit = getUnit(geometry ? \"m3\" : draft.unit);\n  if (!unit || ![\"volume\", \"mass\"].includes(unit.kind)) return { quantity: null, errors: [\"Choose a supported quantity unit.\"] };\n  const result: QuantityPayload = { state: \"specified\", kind: unit.kind, unitId: unit.id, basis: draft.basis, moistureBasis: draft.moistureBasis,\n    method: geometry ? \"calculated\" : draft.mode === \"measured\" ? \"measured\" : \"stated\", accuracy: geometry || draft.mode === \"approximate\" ? \"approximate\" : \"stated\",\n    ...(draft.source.trim() ? { source: draft.source.trim() } : {}),\n    ...(draft.spatialReference ? { spatialReference: draft.spatialReference } : {}) };\n  if (geometry) {\n    const inputs: Record<string, { value: number; unitId: string }> = {};\n    const input = (label: \"length\" | \"width\" | \"depth\" | \"area\", id: string, kind: string) => {\n      const value = number(draft[label]); const definition = getUnit(id, kind);\n      if (value === undefined || !definition) { errors.push(`Enter a positive ${label} and a compatible unit.`); return 0; }\n      inputs[label] = { value, unitId: id }; return value * definition.factorToBase;\n    };\n    const depth = input(\"depth\", draft.depthUnit, \"length\");\n    result.value = draft.mode === \"dimensions\"\n      ? input(\"length\", draft.lengthUnit, \"length\") * input(\"width\", draft.lengthUnit, \"length\") * depth\n      : input(\"area\", draft.areaUnit, \"area\") * depth;\n    result.derivation = { method: draft.mode === \"dimensions\" ? \"box\" : \"area-depth\", version: \"1\", inputs };\n  } else if (draft.mode === \"range\") {\n    result.min = number(draft.min); result.max = number(draft.max);\n    if (result.min === undefined || result.max === undefined || result.min > result.max) errors.push(\"Enter a positive range with the minimum no greater than the maximum.\");\n  } else {\n    result.value = number(draft.value);\n    if (result.value === undefined) errors.push(\"Enter a positive quantity.\");\n  }\n  if ([result.value, result.min, result.max].some(n => n !== undefined && (!Number.isFinite(n) || n <= 0))) errors.push(\"The calculated quantity is outside the supported range.\");\n  if (draft.mode === \"measured\" && !draft.source.trim()) errors.push(\"Add the source of the measured quantity.\");\n  if (errors.length) return { quantity: null, errors };\n  if (draft.targetUnit) {\n    const target = getUnit(draft.targetUnit);\n    const unavailable = (reason: string) => { result.conversion = { state: \"unavailable\", targetUnitId: draft.targetUnit, reason }; };\n    if (!target || ![\"mass\", \"volume\"].includes(target.kind)) unavailable(\"Choose a supported mass or volume unit.\");\n    else {\n      let factor = unit.factorToBase / target.factorToBase;\n      let density: NonNullable<QuantityPayload[\"conversion\"]>[\"density\"];\n      if (unit.kind !== target.kind) {\n        const reviewed = compatibleDensityReferences(context.densityReferences, context.classKey, draft.basis, draft.moistureBasis, context.catalogueVersion)\n          .find(reference => reference.id === draft.densityReferenceId);\n        if (draft.densityReferenceId && !reviewed) unavailable(\"Choose a compatible reviewed UMM density reference, or clear it to enter an explicit density.\");\n        else if (reviewed) {\n          const catalogueVersion = reviewed.catalogueVersion ?? context.catalogueVersion;\n          if (!catalogueVersion) unavailable(\"The selected density reference is missing its catalogue version.\");\n          else {\n            factor *= unit.kind === \"volume\" ? reviewed.mid * 1000 : 1 / (reviewed.mid * 1000);\n            density = {\n              value: reviewed.mid, unit: \"t/m3\", basis: reviewed.basis as VolumeBasis,\n              moistureBasis: reviewed.moistureBasis as MoistureBasis, source: reviewed.source,\n              verification: \"reviewed-reference\",\n              reference: { id: reviewed.id, version: reviewed.version, catalogueVersion },\n            };\n          }\n        } else {\n          const value = number(draft.density);\n          if (!value || !draft.densitySource.trim()) unavailable(\"Add a density and its source to estimate mass or volume.\");\n          else if (draft.basis === \"unknown\" || draft.moistureBasis === \"unknown\" || draft.densityBasis !== draft.basis || draft.densityMoistureBasis !== draft.moistureBasis)\n            unavailable(\"Quantity and density need matching, known physical and moisture bases.\");\n          else {\n            factor *= unit.kind === \"volume\" ? value * 1000 : 1 / (value * 1000);\n            density = { value, unit: \"t/m3\", basis: draft.densityBasis, moistureBasis: draft.densityMoistureBasis, source: draft.densitySource.trim(), verification: \"not-assessed\" };\n          }\n        }\n      }\n      if (!result.conversion) {\n        const converted = { ...(result.value !== undefined ? { value: result.value * factor } : {}), ...(result.min !== undefined ? { min: result.min * factor, max: result.max! * factor } : {}) };\n        if (Object.values(converted).some(n => !Number.isFinite(n) || n <= 0)) unavailable(\"The converted quantity is outside the supported range.\");\n        else result.conversion = { state: \"available\", targetUnitId: target.id, ...converted, estimated: Boolean(density) || result.accuracy === \"approximate\", ...(density ? { density } : {}) };\n      }\n    }\n  }\n  return { quantity: result, errors };\n}\n\nexport function quantityLabel(quantity: QuantityPayload | null, units: readonly QuantityUnit[]): string {\n  if (!quantity) return \"Not added\";\n  if (quantity.state === \"unknown\") return \"Quantity to confirm\";\n  const format = (n: number) => n > 0 && n < 0.001 ? n.toPrecision(3) : new Intl.NumberFormat(\"en-GB\", { maximumFractionDigits: 3 }).format(n);\n  const amount = quantity.value !== undefined ? format(quantity.value) : `${format(quantity.min!)}–${format(quantity.max!)}`;\n  return `${quantity.accuracy === \"approximate\" ? \"≈ \" : \"\"}${amount} ${units.find(u => u.id === quantity.unitId)?.symbol ?? quantity.unitId}`;\n}\n",
      "type": "registry:component",
      "target": "components/blocks/domain/material-selection/material-quantity-model.ts"
    },
    {
      "path": "components/blocks/domain/material-selection/material-quantity.tsx",
      "content": "\"use client\"\n\nimport { useId } from \"react\"\nimport { Button } from \"@/components/ui/button\"\nimport { Collapsible, CollapsibleContent, CollapsibleTrigger } from \"@/components/ui/collapsible\"\nimport { Field, FieldDescription, FieldLabel } from \"@/components/ui/field\"\nimport { Input } from \"@/components/ui/input\"\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@/components/ui/select\"\nimport { MaterialChoiceField } from \"./material-choice-field\"\nimport { compatibleDensityReferences, quantityLabel, quantityResult, type DensityReference, type QuantityDraft, type QuantityUnit } from \"./material-quantity-model\"\n\nexport function MaterialQuantity({ value, units, classKey, densityReferences, catalogueVersion, onChange }: {\n  value: QuantityDraft\n  units: readonly QuantityUnit[]\n  classKey?: string\n  densityReferences?: readonly DensityReference[]\n  catalogueVersion?: string\n  onChange: (value: QuantityDraft) => void\n}) {\n  const id = useId()\n  const patch = (next: Partial<QuantityDraft>) => {\n    const nextValue = { ...value, ...next }\n    // A reviewed reference is basis-specific. Clear it when the user changes\n    // either basis so a stale choice cannot keep driving a conversion.\n    if ((next.basis !== undefined || next.moistureBasis !== undefined) && nextValue.densityReferenceId) {\n      const compatible = compatibleDensityReferences(densityReferences, classKey, nextValue.basis, nextValue.moistureBasis, catalogueVersion)\n      if (!compatible.some(reference => reference.id === nextValue.densityReferenceId)) nextValue.densityReferenceId = \"\"\n    }\n    onChange(nextValue)\n  }\n  const result = quantityResult(value, units, { classKey, densityReferences, catalogueVersion })\n  const geometry = value.mode === \"dimensions\" || value.mode === \"area-depth\"\n  const sourceKind = geometry ? \"volume\" : units.find(u => u.id === value.unit)?.kind\n  const crossDimension = value.targetUnit && units.find(u => u.id === value.targetUnit)?.kind !== sourceKind\n  const input = (key: \"value\" | \"min\" | \"max\" | \"length\" | \"width\" | \"depth\" | \"area\" | \"density\", label: string) => <Field key={key}><FieldLabel htmlFor={`${id}-${key}`}>{label}</FieldLabel><Input id={`${id}-${key}`} inputMode=\"decimal\" value={value[key]} onChange={e => patch({ [key]: e.target.value })} /></Field>\n  const select = (key: \"unit\" | \"targetUnit\" | \"lengthUnit\" | \"depthUnit\" | \"areaUnit\", label: string, options: readonly QuantityUnit[]) => <Field><FieldLabel htmlFor={`${id}-${key}`}>{label}</FieldLabel><Select items={[...(key === \"targetUnit\" ? [{ value: \"\", label: \"None\" }] : []), ...options.map(u => ({ value: u.id, label: `${u.label} (${u.symbol})` }))]} value={value[key]} onValueChange={next => { if (typeof next === \"string\") patch({ [key]: next }) }}><SelectTrigger id={`${id}-${key}`} className=\"w-full\"><SelectValue /></SelectTrigger><SelectContent alignItemWithTrigger={false}>{key === \"targetUnit\" && <SelectItem value=\"\">None</SelectItem>}{options.map(u => <SelectItem key={u.id} value={u.id}>{u.label} ({u.symbol})</SelectItem>)}</SelectContent></Select></Field>\n  const quantityUnits = units.filter(u => [\"mass\", \"volume\"].includes(u.kind))\n  const reviewedReferences = compatibleDensityReferences(densityReferences, classKey, value.basis, value.moistureBasis, catalogueVersion)\n  const selectedReference = reviewedReferences.find(reference => reference.id === value.densityReferenceId)\n  return <div className=\"space-y-5 rounded-xl border p-4\">\n    <MaterialChoiceField<QuantityDraft[\"mode\"]> label=\"How much material?\" description=\"Optional. Enter an amount or work it out from dimensions.\" value={value.mode} onChange={mode => patch({ mode })} options={[[\"exact\", \"Amount\"], [\"measured\", \"Measured\"], [\"approximate\", \"Approximate\"], [\"range\", \"Range\"], [\"dimensions\", \"Dimensions\"], [\"area-depth\", \"Area × depth\"], [\"unknown\", \"Not sure yet\"]]} />\n    {value.mode && value.mode !== \"unknown\" && <>\n      <div className=\"grid gap-4 sm:grid-cols-2\">\n        {geometry ? <>\n          {value.mode === \"dimensions\" ? <>{input(\"length\", \"Length\")}{input(\"width\", \"Width\")}{select(\"lengthUnit\", \"Length and width unit\", units.filter(u => u.kind === \"length\"))}</> : <>{input(\"area\", \"Area\")}{select(\"areaUnit\", \"Area unit\", units.filter(u => u.kind === \"area\"))}</>}\n          {input(\"depth\", \"Depth\")}{select(\"depthUnit\", \"Depth unit\", units.filter(u => u.kind === \"length\"))}\n        </> : <>{value.mode === \"range\" ? <>{input(\"min\", \"Minimum amount\")}{input(\"max\", \"Maximum amount\")}</> : input(\"value\", \"Amount\")}{select(\"unit\", \"Quantity unit\", quantityUnits.filter(u => [\"m3\", \"t\"].includes(u.id) || u.id === value.unit))}</>}\n      </div>\n      {!geometry && <Collapsible><CollapsibleTrigger render={<Button variant=\"link\" className=\"h-auto p-0\" />}>Other quantity units</CollapsibleTrigger><CollapsibleContent className=\"pt-3\">{select(\"unit\", \"International quantity unit\", quantityUnits)}</CollapsibleContent></Collapsible>}\n      <Collapsible key={value.mode} defaultOpen={value.mode === \"measured\"}><CollapsibleTrigger render={<Button variant=\"link\" className=\"h-auto p-0\" />}>Quantity source{value.mode === \"measured\" ? \" (required)\" : \" (optional)\"}</CollapsibleTrigger><CollapsibleContent className=\"pt-3\"><Field><FieldLabel htmlFor={`${id}-quantity-source`}>Where did this quantity come from?</FieldLabel><Input id={`${id}-quantity-source`} value={value.source} onChange={e => patch({ source: e.target.value })} placeholder=\"Survey, weighbridge ticket or project estimate\" /><FieldDescription>A source records your information; it does not verify the measurement.</FieldDescription></Field></CollapsibleContent></Collapsible>\n      <MaterialChoiceField<QuantityDraft[\"basis\"]> label=\"Material handling basis\" description=\"This records the physical state of the quantity. Keep it unknown when the source does not say.\" value={value.basis} onChange={basis => patch({ basis })} options={[[\"unknown\", \"Not sure\"], [\"in-situ\", \"In situ\"], [\"loose\", \"Loose\"], [\"compacted\", \"Compacted\"]]} />\n      <p className=\"text-sm font-medium\" role=\"status\">{result.quantity ? quantityLabel(result.quantity, units) : \"Complete the amount above.\"}</p>\n      {result.errors.map(error => <p key={error} className=\"text-sm text-destructive\" role=\"alert\">{error}</p>)}\n      <Collapsible><CollapsibleTrigger render={<Button variant=\"link\" className=\"h-auto p-0\" />}>Show in another unit</CollapsibleTrigger><CollapsibleContent className=\"space-y-4 pt-4\">\n        {select(\"targetUnit\", \"Display in\", quantityUnits)}\n        {crossDimension && <>\n          <p className=\"text-sm text-muted-foreground\">Mass and volume need a compatible material density. The original quantity stays unchanged; the second unit is an estimate.</p>\n          <MaterialChoiceField<QuantityDraft[\"moistureBasis\"]> label=\"Quantity moisture basis\" value={value.moistureBasis} onChange={moistureBasis => patch({ moistureBasis })} options={[[\"unknown\", \"Not sure\"], [\"as-received\", \"As received\"], [\"dry\", \"Dry\"]]} />\n          {reviewedReferences.length > 0 && <Field><FieldLabel htmlFor={`${id}-density-reference`}>Density source</FieldLabel><Select value={value.densityReferenceId} onValueChange={next => { if (typeof next === \"string\") patch({ densityReferenceId: next }) }}><SelectTrigger id={`${id}-density-reference`} className=\"w-full\"><SelectValue placeholder=\"Choose a density source…\" /></SelectTrigger><SelectContent alignItemWithTrigger={false}><SelectItem value=\"\">Enter a density yourself</SelectItem>{reviewedReferences.map(reference => <SelectItem key={`${reference.id}:${reference.version}`} value={reference.id}>{reference.source} · {reference.mid} t/m³</SelectItem>)}</SelectContent></Select><FieldDescription>Only reviewed UMM references with matching physical and moisture bases appear here.</FieldDescription></Field>}\n          {selectedReference ? <div className=\"space-y-2 rounded-lg border bg-muted/40 p-3 text-sm\"><p className=\"font-medium\">Reviewed UMM reference · {selectedReference.min}–{selectedReference.max} t/m³</p><p className=\"text-muted-foreground\">Using the midpoint {selectedReference.mid} t/m³ for an estimate. {selectedReference.source}</p><p className=\"text-xs text-muted-foreground\">Reference {selectedReference.id} · version {selectedReference.version} · catalogue {selectedReference.catalogueVersion ?? catalogueVersion ?? \"unknown\"}</p><Button variant=\"ghost\" size=\"sm\" onClick={() => patch({ densityReferenceId: \"\" })}>Use my own density</Button></div> : <><p className=\"text-sm text-muted-foreground\">{reviewedReferences.length ? \"No reviewed reference selected. Your value stays an unverified assumption.\" : \"No compatible reviewed UMM density is available for these bases. You can enter an unverified assumption.\"}</p>{input(\"density\", \"Density (t/m³)\")}<Field><FieldLabel htmlFor={`${id}-source`}>Density source</FieldLabel><Input id={`${id}-source`} value={value.densitySource} onChange={e => patch({ densitySource: e.target.value })} placeholder=\"For example, a dated test report or your estimate\" /><FieldDescription>Your density is retained as an unverified assumption.</FieldDescription></Field><MaterialChoiceField<QuantityDraft[\"densityBasis\"]> label=\"Density volume basis\" value={value.densityBasis} onChange={densityBasis => patch({ densityBasis })} options={[[\"unknown\", \"Not sure\"], [\"in-situ\", \"In situ\"], [\"loose\", \"Loose\"], [\"compacted\", \"Compacted\"]]} /><MaterialChoiceField<QuantityDraft[\"densityMoistureBasis\"]> label=\"Density moisture basis\" value={value.densityMoistureBasis} onChange={densityMoistureBasis => patch({ densityMoistureBasis })} options={[[\"unknown\", \"Not sure\"], [\"as-received\", \"As received\"], [\"dry\", \"Dry\"]]} /></>}\n        </>}\n        {result.quantity?.conversion && <p role=\"status\" className=\"text-sm\">{result.quantity.conversion.state === \"unavailable\" ? result.quantity.conversion.reason : `${result.quantity.conversion.estimated ? \"Estimated: \" : \"Converted: \"}${quantityLabel({ ...result.quantity.conversion, state: \"specified\", unitId: value.targetUnit }, units)}`}</p>}\n      </CollapsibleContent></Collapsible>\n    </>}\n    {value.mode && <Button variant=\"ghost\" size=\"sm\" onClick={() => patch({ mode: \"\" })}>Remove quantity</Button>}\n  </div>\n}\n",
      "type": "registry:component",
      "target": "components/blocks/domain/material-selection/material-quantity.tsx"
    },
    {
      "path": "components/blocks/domain/material-selection/material-reference-validation.ts",
      "content": "import type { SelectionReference } from \"./material-selection-model\"\nimport { isQuantityUnit } from \"./material-quantity-model\"\nconst record = (value: unknown): value is Record<string, unknown> => typeof value === \"object\" && value !== null && !Array.isArray(value)\nconst strings = (value: unknown): value is string[] => Array.isArray(value) && value.every(v => typeof v === \"string\")\nconst rows = (value: unknown, test: (row: Record<string, unknown>) => boolean) => Array.isArray(value) && value.every(row => record(row) && test(row))\nconst text = (row: Record<string, unknown>, keys: string[]) => keys.every(key => typeof row[key] === \"string\")\nconst nullable = (value: unknown) => value === null || typeof value === \"string\"\nconst unique = (values: unknown[]) => new Set(values).size === values.length\n/** Check every reference field consumed by the portable controls before using remote data. */\nexport function isSelectionReference(value: unknown, taxonomyVersion: string): value is SelectionReference {\n  return record(value) && value.taxonomyVersion === taxonomyVersion && text(value, [\"version\", \"language\"])\n    && (value.designationDefaults === undefined || (record(value.designationDefaults) && Object.values(value.designationDefaults).every(v => typeof v === \"string\")))\n    && record(value.materialOrigins) && Object.values(value.materialOrigins).every(v => strings(v) && v.every(s => [\"primary\", \"recycled\", \"secondary\", \"blended\"].includes(s)))\n    && rows(value.specificationApplicability, row => text(row, [\"specificationKey\", \"relation\", \"reviewStatus\", \"conditions\"]) && strings(row.classKeys) && typeof row.selectable === \"boolean\")\n    && rows(value.standardReferences, row => text(row, [\"id\", \"specificationKey\", \"publisher\", \"document\", \"status\", \"designation\", \"reviewStatus\"]) && nullable(row.edition) && nullable(row.sourceUrl) && strings(row.jurisdictions))\n    && rows(value.uses, row => text(row, [\"key\", \"label\", \"description\", \"question\", \"relation\", \"reviewStatus\"]) && strings(row.classKeys) && strings(row.specificationKeys))\n    && rows(value.quantityUnits, row => isQuantityUnit(row))\n    && Array.isArray(value.quantityUnits) && unique(value.quantityUnits.map(row => record(row) ? row.id : undefined))\n    && rows(value.densityReferences, row => text(row, [\"id\", \"classKey\", \"version\", \"unit\", \"basis\", \"moistureBasis\", \"source\", \"reviewStatus\"]) && (row.catalogueVersion === undefined || typeof row.catalogueVersion === \"string\") && (row.review === undefined || (record(row.review) && text(row.review, [\"id\", \"reviewedAt\", \"finding\", \"reason\"]))) && (!row.usableForConversion || (row.reviewStatus === \"reviewed-usable\" && record(row.review) && text(row.review, [\"id\", \"reviewedAt\", \"finding\", \"reason\"]))) && [row.min, row.mid, row.max].every(n => typeof n === \"number\" && Number.isFinite(n) && n > 0) && Number(row.min) <= Number(row.mid) && Number(row.mid) <= Number(row.max) && typeof row.usableForConversion === \"boolean\")\n    && Array.isArray(value.densityReferences) && unique(value.densityReferences.map(row => record(row) ? row.id : undefined))\n}\n",
      "type": "registry:component",
      "target": "components/blocks/domain/material-selection/material-reference-validation.ts"
    },
    {
      "path": "components/blocks/domain/material-selection/material-selection-field.tsx",
      "content": "\"use client\"\n\nimport { useId, useState } from \"react\"\nimport { ChevronDown } from \"@untitledui/icons\"\nimport { Button } from \"@/components/ui/button\"\nimport { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from \"@/components/ui/dialog\"\nimport { Field, FieldDescription, FieldLabel } from \"@/components/ui/field\"\nimport { MaterialBrowser } from \"./material-browser\"\nimport { materialLabel, blankDraft, chooseMaterial, chooseSpecification, hasSelection, type MaterialDraft, type MaterialReference, type MaterialVisuals } from \"./material-selection-model\"\n\n/** A compact form field using the same search/browse contract as the full flow. */\nexport function MaterialSelectionField({ data, visuals, value, onValueChange, label = \"Material\" }: {\n  data: MaterialReference; visuals?: MaterialVisuals; value: MaterialDraft; onValueChange: (value: MaterialDraft) => void; label?: string\n}) {\n  const [open, setOpen] = useState(false)\n  const id = useId()\n  const material = data.classes.find(c => c.key === value.material?.classKey)\n  const emptyLabel = value.intent === \"need\" ? \"Choose material needed…\" : \"Choose material available…\"\n  return <Field><FieldLabel htmlFor={id}>{label}</FieldLabel><Dialog open={open} onOpenChange={setOpen}>\n    <DialogTrigger id={id} render={<Button variant=\"secondary\" className=\"w-full justify-between\" />}><span className=\"truncate\">{material ? materialLabel(data, material) : (hasSelection(value) ? \"Material to identify\" : emptyLabel)}</span><ChevronDown /></DialogTrigger>\n    <DialogContent className=\"max-h-[85svh] overflow-y-auto sm:max-w-xl\"><DialogHeader><DialogTitle>{value.intent === \"need\" ? \"Choose material needed\" : \"Choose material available\"}</DialogTitle><DialogDescription>{value.intent === \"need\" ? \"Search, browse or describe what your project needs.\" : \"Search, browse or describe what is available.\"}</DialogDescription></DialogHeader><MaterialBrowser key={open ? \"open\" : \"closed\"} data={data} visuals={visuals} intent={value.intent} compact onSelectSpecification={key => { onValueChange(chooseSpecification(value, key)); setOpen(false) }} onSelect={option => { onValueChange(chooseMaterial(value, option, data)); setOpen(false) }} onDescribe={description => { onValueChange({ ...blankDraft(value.intent), description }); setOpen(false) }} /></DialogContent>\n  </Dialog><FieldDescription>{value.description || (value.intent === \"need\" ? \"Add requirements such as preparation, origin and standards.\" : \"Record what you know, including preparation, origin and supporting documents.\")}</FieldDescription></Field>\n}\n",
      "type": "registry:component",
      "target": "components/blocks/domain/material-selection/material-selection-field.tsx"
    },
    {
      "path": "components/blocks/domain/material-selection/material-selection-model.ts",
      "content": "import { validateSelection } from \"./material-selection-validation\";\nimport { blankQuantity, quantityResult, type QuantityDraft, type QuantityUnit, type DensityReference } from \"./material-quantity-model\";\n/** Minimal UMM contracts. The host owns fetching, release pinning and visual URLs. */\nexport interface MaterialClass {\n  key: string; parentKey: string | null; name: string; isLeaf: boolean;\n  synonyms: readonly string[]; sort: number;\n}\nexport interface MaterialTerm {\n  code: string; label: string; parent: string; synonyms: readonly string[];\n  shortLabel?: string; specTemplateKey?: string;\n}\nexport interface MaterialSpecification { key: string; name: string; classKey: string }\nexport interface StandardReference {\n  id: string; specificationKey: string; publisher: string; document: string;\n  edition: string | null; jurisdictions: readonly string[]; sourceUrl: string | null;\n  status: string; designation: string; reviewStatus: string;\n}\nexport interface MaterialUse {\n  key: string; label: string; description: string; classKeys: readonly string[];\n  specificationKeys: readonly string[]; question: string; relation: string; reviewStatus: string;\n}\nexport interface SelectionReference {\n  version: string; taxonomyVersion: string; language: string;\n  designationDefaults?: Readonly<Record<string, string>>;\n  materialOrigins: Readonly<Record<string, readonly string[]>>;\n  specificationApplicability: readonly { specificationKey: string; classKeys: readonly string[]; relation: string; reviewStatus: string; selectable: boolean; conditions: string }[];\n  standardReferences: readonly StandardReference[]; uses: readonly MaterialUse[];\n  quantityUnits: readonly QuantityUnit[]; densityReferences: readonly DensityReference[];\n}\nexport interface MaterialReference {\n  profile?: MaterialCatalogueProfile;\n  selectionReference?: SelectionReference;\n  version: string; classes: readonly MaterialClass[]; terms: readonly MaterialTerm[];\n  specTemplates: readonly MaterialSpecification[];\n}\nexport interface MaterialVisual { url: string; alt?: string }\nexport type MaterialVisuals = Readonly<Record<string, MaterialVisual>>;\n\n\n/** Prototype presentation policy, separate from the immutable reference catalogue. */\nexport const PRESENTATION_VERSION = \"material-selection.2\";\nexport type Intent = \"need\" | \"have\";\nexport type Answer = \"\" | \"unknown\" | \"none\";\nexport type Processing = Answer | \"as-dug\" | \"screened\" | \"unscreened\";\nexport type Origin = Answer | \"primary\" | \"secondary\" | \"recycled\" | \"blended\";\nexport interface MaterialDraft {\n  intent: Intent;\n  material: { classKey: string; termCode?: string } | null;\n  description: string;\n  processing: Processing;\n  origin: Origin;\n  originStrength: \"required\" | \"preferred\";\n  specification: string;\n  customSpecification: string;\n  additionalSpecifications: string[];\n  specificationOperator: \"all\" | \"any\";\n  useKey: string;\n  quantity: QuantityDraft;\n  evidence: \"\" | \"unknown\" | \"available\" | \"none\";\n  purpose: string;\n  notes: string;\n}\nexport const blankDraft = (intent: Intent): MaterialDraft => ({\n  intent,\n  material: null,\n  description: \"\",\n  processing: \"\",\n  origin: \"\",\n  originStrength: \"required\",\n  specification: \"\",\n  customSpecification: \"\",\n  additionalSpecifications: [],\n  specificationOperator: \"all\",\n  useKey: \"\",\n  quantity: blankQuantity(),\n  evidence: \"\",\n  purpose: \"\",\n  notes: \"\",\n});\n\n/** The API/host owns product scope; canonical classes keep their original identities. */\nexport interface MaterialGroup {\n  key: string; label: string; detail: string; classKeys: readonly string[]; visualClassKey?: string;\n}\nexport interface MaterialCatalogueProfile {\n  id: string; version: string; taxonomyVersion: string;\n  groups: readonly MaterialGroup[];\n  popularClassKeys: readonly string[];\n  labels: Readonly<Record<string, string>>;\n  aliases: Readonly<Record<string, readonly string[]>>;\n  visualClassKeys: readonly string[];\n  defaultQuantityUnits?: Readonly<Record<string, string>>;\n}\nexport const materialLabel = (data: MaterialReference, material: MaterialClass) =>\n  data.profile?.labels[material.key] ?? material.name;\nexport const materialGroups = (data: MaterialReference): readonly MaterialGroup[] => {\n  if (data.profile) return data.profile.groups;\n  // Generic consumers without a product profile can browse their supplied reference data.\n  const roots = data.classes.filter(c => !c.parentKey);\n  for (const key of new Set(data.classes.map(c => c.parentKey).filter((key): key is string => Boolean(key)))) {\n    if (!data.classes.some(c => c.key === key)) roots.push({ key, name: key === \"soil\" ? \"Soils\" : key, parentKey: null, isLeaf: false, synonyms: [], sort: 0 });\n  }\n  return roots.map(root => ({ key: root.key === \"soil\" ? \"soils\" : root.key, label: root.name, detail: \"\",\n    classKeys: data.classes.filter(c => c.isLeaf && descendantOf(data, c.key, root.key)).map(c => c.key) }));\n};\nexport const isSelectable = (data: MaterialReference, key: string) =>\n  !data.profile || data.profile.groups.some(group => group.classKeys.includes(key));\nexport const popularMaterials = (data: MaterialReference) =>\n  (data.profile?.popularClassKeys ?? [\"soil.topsoil\", \"soil.subsoil\", \"aggregates.unbound.type1\", \"aggregates.crushed.6f2\"])\n    .filter(key => isSelectable(data, key));\n/** Follow explicit parents; a dotted ID is opaque, not a relationship. */\nexport function descendantOf(data: MaterialReference, key: string, root: string): boolean {\n  const visited = new Set<string>(); let current: string | null = key;\n  while (current && !visited.has(current)) {\n    if (current === root) return true;\n    visited.add(current); current = data.classes.find(c => c.key === current)?.parentKey ?? null;\n  }\n  return false;\n}\nexport const originsFor = (data: MaterialReference, key: string): readonly string[] =>\n  data.selectionReference?.materialOrigins[key] ?? (hasOrigin(key) ? intrinsicOrigin(key) ? [intrinsicOrigin(key)!] : [\"primary\", \"recycled\", \"secondary\", \"blended\"] : []);\nexport const specificationsFor = (data: MaterialReference, key?: string) => data.specTemplates.filter(spec => {\n  const mapping = data.selectionReference?.specificationApplicability.find(row => row.specificationKey === spec.key);\n  return mapping ? mapping.selectable && (!key || mapping.classKeys.includes(key)) : !key || descendantOf(data, key, spec.classKey);\n});\nexport const standardReference = (data: MaterialReference, key: string) => data.selectionReference?.standardReferences.find(row => row.specificationKey === key);\n// Keep duplicates visible until the contract validator rejects them. Silently\n// deduplicating here would hide a malformed multi-standard draft and make the\n// validation branch unreachable.\nexport const selectedSpecifications = (draft: MaterialDraft) => [draft.specification, ...draft.additionalSpecifications].filter(s => s && ![\"unknown\", \"none\"].includes(s));\nexport const hasSelection = (draft: MaterialDraft) => Boolean(draft.material || draft.description.trim() || selectedSpecifications(draft).length);\nexport function chooseSpecification(draft: MaterialDraft, key: string): MaterialDraft {\n  return { ...blankDraft(draft.intent), specification: `template:${key}` };\n}\nexport const inBranch = (key: string, root: string) =>\n  key === root || key.startsWith(`${root}.`);\nexport const hasProcessing = (key: string) =>\n  [\"soil.topsoil\", \"soil.subsoil\", \"soil.fill\"].includes(key) || hasOrigin(key);\nexport const hasOrigin = (key: string) =>\n  inBranch(key, \"aggregates\") || inBranch(key, \"recycled\") || key === \"asphalt.arisings.planings\";\nexport const intrinsicOrigin = (key: string): Origin | undefined =>\n  (inBranch(key, \"recycled\") || key === \"asphalt.arisings.planings\")\n    ? \"recycled\"\n    : inBranch(key, \"aggregates.secondary\")\n      ? \"secondary\"\n      : undefined;\nexport const normalise = (text: string) =>\n  text\n    .toLowerCase()\n    .replace(/bs\\s*3882/g, \"bs3882\")\n    .replace(/bs\\s*8601/g, \"bs8601\")\n    .replace(/type\\s*([123])/g, \"type$1\")\n    .replace(/[^a-z0-9]+/g, \" \")\n    .trim();\n\nexport interface MaterialOption {\n  material: MaterialClass;\n  termCode?: string;\n  matchedName?: string;\n  processing?: Processing;\n  origin?: Origin;\n  specification?: string;\n  useKey?: string;\n}\n\n/** One result per class. Qualifiers remain explicit suggestions, never new taxonomy keys. */\nexport function findMaterials(\n  data: MaterialReference,\n  query: string,\n  group = \"\",\n  filters: { origin?: string; useKey?: string; specificationKey?: string } = {},\n): MaterialOption[] {\n  let words = normalise(query);\n  let processing: Processing | undefined;\n  let origin: Origin | undefined;\n  const extract = (pattern: RegExp) => {\n    if (!pattern.test(words)) return false;\n    words = words.replace(pattern, \" \").trim();\n    return true;\n  };\n  if (extract(/\\bas dug\\b/)) processing = \"as-dug\";\n  else if (extract(/\\bunscreened\\b/)) processing = \"unscreened\";\n  else if (extract(/\\bscreened\\b/)) processing = \"screened\";\n  if (extract(/\\brecycled\\b/)) origin = \"recycled\";\n  else if (extract(/\\b(primary|virgin)\\b/)) origin = \"primary\";\n  else if (extract(/\\bsecondary\\b/)) origin = \"secondary\";\n  else if (extract(/\\bblended\\b/)) origin = \"blended\";\n  if (filters.origin && origin && filters.origin !== origin) return [];\n  if (filters.useKey && !data.selectionReference?.uses.some(use => use.key === filters.useKey)) return [];\n  const requestedOrigin = filters.origin || origin;\n  const tokens = words.split(/\\s+/).filter(Boolean);\n  const groupKeys = materialGroups(data).find(g => g.key === group)?.classKeys;\n  const popularKeys = popularMaterials(data);\n  const orderedKeys = data.profile?.groups.flatMap(g => [...g.classKeys]);\n  const exactKeys = new Set<string>();\n  return data.classes\n    .filter(\n      (c) => c.isLeaf && isSelectable(data, c.key) && (!group || groupKeys?.includes(c.key)),\n    )\n    .flatMap((material): MaterialOption[] => {\n      const use = data.selectionReference?.uses.find(row => row.key === filters.useKey);\n      if (use && !use.classKeys.includes(material.key)) return [];\n      if (filters.specificationKey && !specificationsFor(data, material.key).some(s => s.key === filters.specificationKey)) return [];\n      if (\n        (processing && !hasProcessing(material.key)) ||\n        (requestedOrigin && !originsFor(data, material.key).includes(requestedOrigin))\n      )\n        return [];\n      const terms = data.terms.filter((t) => t.parent === material.key);\n      const specs = specificationsFor(data, material.key);\n      const names = [\n          materialLabel(data, material),\n          ...(data.profile?.aliases[material.key] ?? []),\n          material.name,\n          ...material.synonyms,\n          ...terms.flatMap((t) => [t.label, t.shortLabel ?? \"\", ...t.synonyms]),\n      ];\n      const haystack = normalise([...names, material.key, ...specs.map(s => s.name)].join(\" \"));\n      if (!tokens.every((token) => haystack.includes(token))) return [];\n      if (words && names.some(name => normalise(name) === words)) exactKeys.add(material.key);\n      const term = words\n        ? terms.find((t) =>\n            [t.label, t.shortLabel ?? \"\", ...t.synonyms].some(\n              (label) => normalise(label) === words,\n            ),\n          )\n        : undefined;\n      const designation =\n        term &&\n        normalise(term.label) !== normalise(material.name) &&\n        term.code !== \"term.topsoil_generic\"\n          ? term\n          : undefined;\n      let specification = term?.code !== \"term.topsoil_gp\" && term?.specTemplateKey && specs.some(s => s.key === term.specTemplateKey) ? `template:${term.specTemplateKey}` : undefined;\n      // A broad BS 3882 query must never silently choose a grade, especially via the legacy GP link.\n      if (material.key === \"soil.topsoil\" && /\\bbs3882\\b/.test(words)) {\n        specification = /\\bmultipurpose\\b/.test(words)\n          ? \"template:BS3882_Multipurpose\"\n          : \"family:BS3882\";\n      }\n      return [\n        {\n          material,\n          termCode: designation?.code,\n          matchedName: designation?.label,\n          processing,\n          origin: origin ?? (filters.origin as Origin | undefined),\n          specification,\n          useKey: filters.useKey,\n        },\n      ];\n    })\n    .sort((a, b) => {\n      // Separate tiers keep exact matches and every promoted item ahead of catalogue order,\n      // regardless of how many common choices a host configures.\n      const rank = (material: MaterialClass) => {\n        const popular = popularKeys.indexOf(material.key);\n        return [exactKeys.has(material.key) ? 0 : 1, popular < 0 ? 1 : 0,\n          popular < 0 ? 0 : popular, orderedKeys ? orderedKeys.indexOf(material.key) : material.sort];\n      };\n      const left = rank(a.material), right = rank(b.material);\n      for (let i = 0; i < left.length; i++) {\n        const aRank = left[i], bRank = right[i];\n        if (aRank !== undefined && bRank !== undefined && aRank !== bRank) return aRank - bRank;\n      }\n      return a.material.key.localeCompare(b.material.key, \"en-GB\");\n    });\n}\n\nexport function chooseMaterial(\n  draft: MaterialDraft,\n  option: MaterialOption,\n  data?: MaterialReference,\n): MaterialDraft {\n  const same = draft.material?.classKey === option.material.key;\n  const unresolved = !draft.material && hasSelection(draft);\n  const retain = same || unresolved;\n  const origins = data ? originsFor(data, option.material.key) : [];\n  const designationDefault = data?.selectionReference?.designationDefaults?.[option.material.key];\n  const fixedOrigin = origins.length === 1 ? origins[0] as Origin : data ? undefined : intrinsicOrigin(option.material.key);\n  return {\n    ...(retain ? draft : blankDraft(draft.intent)),\n    material: {\n      classKey: option.material.key,\n      ...(option.termCode ? { termCode: option.termCode } : {}),\n    },\n    processing: option.processing ?? (retain ? draft.processing : \"\"),\n    origin:\n      fixedOrigin ??\n      option.origin ??\n      (retain ? draft.origin : \"\"),\n    specification: option.specification ?? (retain && draft.specification ? draft.specification : designationDefault ? `template:${designationDefault}` : \"\"),\n    useKey: option.useKey ?? (retain ? draft.useKey : \"\"),\n    quantity: retain && draft.quantity.mode ? draft.quantity : blankQuantity(data?.profile?.defaultQuantityUnits?.[option.material.key] ?? \"m3\"),\n  };\n}\n\nexport const answerLabel = (value: string, intent: Intent) => {\n  if (value === \"unknown\") return \"Not sure yet\";\n  if (value === \"none\")\n    return intent === \"need\" ? \"No preference\" : \"None specified\";\n  if (!value) return \"Not added\";\n  return value === \"as-dug\"\n    ? \"As-dug\"\n    : value.charAt(0).toUpperCase() + value.slice(1);\n};\nexport function specificationLabel(\n  draft: MaterialDraft,\n  data: MaterialReference,\n) {\n  if (draft.specification === \"family:BS3882\")\n    return \"BS 3882 · grade to confirm\";\n  if (draft.specification === \"custom\")\n    return (\n      draft.customSpecification.trim() ||\n      \"Custom specification · details to add\"\n    );\n  if (draft.specification.startsWith(\"template:\"))\n    return (\n      data.specTemplates.find((s) => s.key === draft.specification.slice(9))\n        ?.name ?? \"Reference unavailable\"\n    );\n  return answerLabel(draft.specification, draft.intent);\n}\n\nexport function selectionPayload(\n  draft: MaterialDraft,\n  data: MaterialReference,\n) {\n  const quantity = quantityResult(draft.quantity, data.selectionReference?.quantityUnits ?? [], {\n    classKey: draft.material?.classKey,\n    densityReferences: data.selectionReference?.densityReferences,\n    catalogueVersion: data.version,\n  });\n  if (quantity.errors.length) throw new Error(quantity.errors.join(\" \"));\n  if (selectedSpecifications(draft).includes(\"custom\") && !draft.customSpecification.trim()) throw new Error(\"Describe the custom specification.\");\n  const answer = (\n    value: string,\n    noneState = draft.intent === \"need\" ? \"no-preference\" : \"none-reported\",\n  ) => ({\n    state: !value\n      ? \"unanswered\"\n      : value === \"unknown\"\n        ? \"unknown\"\n        : value === \"none\"\n          ? noneState\n          : \"specified\",\n    value: value || null,\n  });\n  const details = {\n    processing: answer(draft.processing),\n    origin: {\n      ...answer(draft.origin),\n      ...(draft.intent === \"need\" &&\n      ![\"\", \"unknown\", \"none\"].includes(draft.origin)\n        ? { strength: draft.originStrength }\n        : {}),\n    },\n    specifications: {\n      state: selectedSpecifications(draft).length ? \"specified\" : answer(draft.specification, draft.intent === \"need\" ? \"not-required\" : \"none-reported\").state,\n      operator: draft.specificationOperator,\n      items: selectedSpecifications(draft).map(value => {\n        if (value.startsWith(\"template:\")) {\n          const key = value.slice(9), ref = standardReference(data, key);\n          return { kind: \"reference\", specificationKey: key,\n            referenceId: ref?.id ?? null, edition: ref?.edition ?? null,\n            jurisdictions: ref?.jurisdictions ?? [], verification: \"not-assessed\" };\n        }\n        if (value === \"family:BS3882\") return { kind: \"family\", standard: \"BS 3882\", gradeState: \"unknown\" };\n        return { kind: \"custom\", text: draft.customSpecification.trim() };\n      }),\n    },\n  };\n  const payload = {\n    schemaVersion: \"material-selection.1-rc.1\",\n    presentationVersion: PRESENTATION_VERSION,\n    taxonomyVersion: data.version,\n    ...(data.profile ? { catalogueProfile: { id: data.profile.id, version: data.profile.version } } : {}),\n    intent: draft.intent,\n    material: draft.material,\n    unclassifiedDescription: draft.material ? null : draft.description.trim(),\n    ...(draft.intent === \"need\"\n      ? { requirements: details }\n      : { reportedProperties: details }),\n    ...(draft.intent === \"have\"\n      ? {\n          evidence: {\n            state: draft.evidence || \"unanswered\",\n            documents: [],\n            verification: \"not-assessed\",\n          },\n        }\n      : {}),\n    intendedUse: draft.useKey || draft.purpose.trim() ? { useKey: draft.useKey || null, details: draft.purpose.trim() || null } : null,\n    quantity: quantity.quantity,\n    notes: draft.notes.trim() || null,\n  };\n  const errors = validateSelection(payload, data);\n  if (errors.length) throw new Error(errors.join(\" \"));\n  return payload;\n}\n",
      "type": "registry:component",
      "target": "components/blocks/domain/material-selection/material-selection-model.ts"
    },
    {
      "path": "components/blocks/domain/material-selection/material-selection-summary.tsx",
      "content": "\"use client\"\n\nimport { ArrowRight, LayersThree01 } from \"@untitledui/icons\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Button } from \"@/components/ui/button\"\nimport { Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from \"@/components/ui/card\"\nimport { quantityLabel, quantityResult } from \"./material-quantity-model\"\nimport { MaterialArtwork } from \"./material-artwork\"\nimport { materialLabel, answerLabel, originsFor, hasSelection, selectionPayload, selectedSpecifications, hasProcessing, specificationLabel, type MaterialDraft, type MaterialReference, type MaterialVisuals } from \"./material-selection-model\"\n\nexport function MaterialSelectionSummary({ data, visuals, value, onConfirm }: {\n  data: MaterialReference; visuals?: MaterialVisuals; value: MaterialDraft; onConfirm?: (value: MaterialDraft) => void\n}) {\n  const need = value.intent === \"need\"\n  const material = data.classes.find(c => c.key === value.material?.classKey)\n  const chosen = hasSelection(value)\n  const quantity = quantityResult(value.quantity, data.selectionReference?.quantityUnits ?? [], {\n    classKey: value.material?.classKey,\n    densityReferences: data.selectionReference?.densityReferences,\n    catalogueVersion: data.version,\n  })\n  let invalid = \"\"\n  try { selectionPayload(value, data) } catch (e) { invalid = e instanceof Error ? e.message : \"Complete the highlighted details.\" }\n  const conversion = quantity.quantity?.conversion\n  const convertedDetail = conversion?.state === \"available\"\n    ? quantityLabel({ state: \"specified\", unitId: conversion.targetUnitId, value: conversion.value, min: conversion.min, max: conversion.max, accuracy: conversion.estimated ? \"approximate\" : \"stated\" }, data.selectionReference?.quantityUnits ?? [])\n    : conversion?.state === \"unavailable\" ? `Unavailable · ${conversion.reason}` : undefined\n  const facts: [string, string][] = [\n    ...(material && hasProcessing(material.key) ? [[\"Preparation\", answerLabel(value.processing, value.intent)] as [string, string]] : []),\n    ...(material && originsFor(data, material.key).length > 0 ? [[\"Origin\", `${answerLabel(value.origin, value.intent)}${need && ![\"\", \"unknown\", \"none\"].includes(value.origin) ? ` · ${value.originStrength}` : \"\"}`] as [string, string]] : []),\n    [need ? \"Required standard\" : \"Reported standard\", selectedSpecifications(value).length > 1 ? `${selectedSpecifications(value).map(specification => specificationLabel({ ...value, specification }, data)).join(value.specificationOperator === \"all\" ? \" + \" : \" OR \")} (${value.specificationOperator === \"all\" ? \"all required\" : \"alternatives\"})` : specificationLabel(value, data)],\n    [\"Quantity\", quantity.errors.length ? \"Amount to complete\" : quantityLabel(quantity.quantity, data.selectionReference?.quantityUnits ?? [])],\n    ...(convertedDetail ? [[\"Other unit\", convertedDetail] as [string, string]] : []),\n    ...(!need ? [[\"Documents\", value.evidence === \"available\" ? \"Available · not reviewed\" : value.evidence === \"none\" ? \"None yet\" : answerLabel(value.evidence, value.intent)] as [string, string]] : []),\n    ...(need && value.purpose.trim() ? [[\"Intended use\", value.purpose] as [string, string]] : []),\n    ...(value.notes.trim() ? [[\"Other details\", value.notes] as [string, string]] : []),\n  ]\n  return <Card className=\"min-w-0\" aria-label=\"Selection summary\" role=\"region\">\n    <CardHeader><CardTitle>{need ? \"Your requirement\" : \"Your material\"}</CardTitle><CardDescription>A clear description, as you go.</CardDescription><CardAction><Badge variant=\"gray\">Draft</Badge></CardAction></CardHeader>\n    <CardContent className=\"flex flex-col gap-5\">\n      {chosen ? <>\n        {material && visuals?.[material.key] && <MaterialArtwork classKey={material.key} visuals={visuals} className=\"h-32 w-full\" />}\n        <div><h3 className=\"text-xl font-semibold\">{material ? materialLabel(data, material) : \"Material to identify\"}</h3><p className=\"mt-1 text-sm text-muted-foreground\">{material ? need ? \"Looking for material\" : \"Material available\" : value.description}</p></div>\n        <dl className=\"divide-y divide-border text-sm\">{facts.map(([label, detail]) => <div key={label} className=\"grid grid-cols-[minmax(0,1fr)_minmax(0,1.4fr)] gap-4 py-3 first:pt-0\"><dt className=\"text-muted-foreground\">{label}</dt><dd className=\"min-w-0 wrap-anywhere font-medium\">{detail}</dd></div>)}</dl>\n        {!need && value.specification && ![\"unknown\", \"none\"].includes(value.specification) && <p className=\"text-sm text-muted-foreground\">Reported by you. Conformity hasn’t been assessed.</p>}\n        {!material && <p className=\"text-sm text-muted-foreground\">The description is kept; material classification is still open.</p>}\n      </> : <div className=\"flex flex-col items-center gap-4 rounded-xl border border-dashed px-5 py-10 text-center\"><div className=\"rounded-lg border bg-background p-3 shadow-xs\"><LayersThree01 className=\"size-6 text-muted-foreground\" /></div><h3 className=\"font-semibold\">Start with what you know</h3><p className=\"text-sm text-muted-foreground\">Choose a material or describe it in your own words. Details can follow.</p></div>}\n    </CardContent>\n    {chosen && onConfirm && <CardFooter className=\"flex-col items-stretch gap-3\"><Button disabled={Boolean(invalid)} onClick={() => onConfirm(value)}>Use this {need ? \"requirement\" : \"material\"}<ArrowRight /></Button>{invalid && <p role=\"alert\" className=\"text-sm text-destructive\">{invalid}</p>}<p className=\"text-center text-xs text-muted-foreground\">You can continue with details still to confirm.</p></CardFooter>}\n  </Card>\n}\n",
      "type": "registry:component",
      "target": "components/blocks/domain/material-selection/material-selection-summary.tsx"
    },
    {
      "path": "components/blocks/domain/material-selection/material-selection-validation.ts",
      "content": "import type { MaterialReference } from \"./material-selection-model\"\nimport { isQuantityUnit, isReviewedDensityReference } from \"./material-quantity-model\"\nconst record = (value: unknown): value is Record<string, unknown> => typeof value === \"object\" && value !== null && !Array.isArray(value)\n/** Semantic checks complement the published JSON Schema; they never assess conformity. */\nexport function validateSelection(value: unknown, data: MaterialReference): string[] {\n  if (!record(value)) return [\"Selection must be an object.\"]\n  const errors: string[] = []\n  if (value.taxonomyVersion !== data.version) errors.push(\"Use the catalogue version recorded in the selection.\")\n  if (record(value.catalogueProfile) && (value.catalogueProfile.id !== data.profile?.id || value.catalogueProfile.version !== data.profile?.version)) errors.push(\"Use the catalogue profile version recorded in the selection.\")\n  const material = record(value.material) ? value.material : undefined\n  if (material && !data.classes.some(c => c.key === material.classKey && c.isLeaf)) errors.push(\"Unknown material identity.\")\n  if (material && data.profile && !data.profile.groups.some(g => g.classKeys.includes(String(material.classKey)))) errors.push(\"Material is outside the selected profile.\")\n  if (material?.termCode && !data.terms.some(t => t.code === material.termCode && t.parent === material.classKey)) errors.push(\"Designation does not belong to the selected material.\")\n  const reference = data.selectionReference\n  const details = record(value.requirements) ? value.requirements : record(value.reportedProperties) ? value.reportedProperties : undefined\n  if (reference && details) {\n    const origin = record(details.origin) ? details.origin : undefined\n    if (material && origin?.state === \"specified\" && !reference.materialOrigins[String(material.classKey)]?.includes(String(origin.value))) errors.push(\"Origin is not an available option for this material.\")\n    const specifications = record(details.specifications) ? details.specifications : undefined\n    const items = Array.isArray(specifications?.items) ? specifications.items : []\n    const seen = new Set<string>()\n    for (const item of items) {\n      if (!record(item)) continue\n      const key = item.kind === \"reference\"\n        ? `reference:${String(item.specificationKey)}`\n        : item.kind === \"family\"\n          ? `family:${String(item.standard)}`\n          : item.kind === \"custom\"\n            ? `custom:${String(item.text).trim().toLowerCase()}`\n            : `unknown:${String(item.kind)}`\n      if (seen.has(key)) errors.push(\"Duplicate specification.\")\n      seen.add(key)\n      if (item.kind !== \"reference\") continue\n      const specificationKey = String(item.specificationKey)\n      const ref = reference.standardReferences.find(row => row.specificationKey === specificationKey)\n      const applicability = reference.specificationApplicability.find(row => row.specificationKey === specificationKey)\n      if (!ref || !applicability?.selectable || !data.specTemplates.some(s => s.key === specificationKey)) errors.push(\"Unknown or reference-only specification.\")\n      else {\n        if (item.referenceId !== ref.id || item.edition !== ref.edition || JSON.stringify(item.jurisdictions) !== JSON.stringify(ref.jurisdictions)) errors.push(\"Specification reference does not match its recorded edition and jurisdiction.\")\n        if (material && !applicability.classKeys.includes(String(material.classKey))) errors.push(\"This standard is not a listed candidate for the selected material. Choose a compatible material or revise the requirement.\")\n      }\n    }\n    const intendedUse = record(value.intendedUse) ? value.intendedUse : undefined\n    if (intendedUse?.useKey && !reference.uses.some(use => use.key === intendedUse.useKey)) errors.push(\"Unknown intended-use reference.\")\n  }\n  const q = record(value.quantity) ? value.quantity : undefined\n  if (!q) return errors\n  if (q.state === \"unknown\") {\n    if (Object.keys(q).some(key => ![\"state\", \"spatialReference\"].includes(key))) errors.push(\"Unknown quantities cannot include amount, unit or conversion details.\")\n    return errors\n  }\n  if (q.state !== \"specified\") {\n    errors.push(\"Quantity state must be unknown or specified.\")\n    return errors\n  }\n  const units = reference?.quantityUnits ?? []\n  const unit = units.find(u => isQuantityUnit(u) && u.id === q.unitId)\n  if (!unit || unit.kind !== q.kind || ![\"volume\", \"mass\"].includes(unit.kind)) return [...errors, \"Unknown or incompatible quantity unit.\"]\n  const amounts = q.value !== undefined ? [q.value] : [q.min, q.max]\n  if (amounts.some(n => typeof n !== \"number\" || !Number.isFinite(n) || n <= 0)) errors.push(\"Quantity values must be finite and positive.\")\n  if (typeof q.min === \"number\" && typeof q.max === \"number\" && q.min > q.max) errors.push(\"Quantity range is inverted.\")\n  const close = (a: unknown, b: number) => typeof a === \"number\" && Number.isFinite(a) && Number.isFinite(b) && Math.abs(a - b) <= Math.max(Math.abs(b) * 1e-10, 1e-12)\n  if (q.method === \"calculated\") {\n    const derivation = record(q.derivation) ? q.derivation : undefined\n    const inputs = record(derivation?.inputs) ? derivation.inputs : undefined\n    const measure = (key: string, kind: string) => {\n      const input = inputs && record(inputs[key]) ? inputs[key] : undefined\n      const u = units.find(u => isQuantityUnit(u) && u.id === input?.unitId && u.kind === kind)\n      if (!u || typeof input?.value !== \"number\" || !Number.isFinite(input.value) || input.value <= 0) { errors.push(`Invalid ${key} input or unit.`); return NaN }\n      return input.value * u.factorToBase\n    }\n    const expected = derivation?.method === \"box\" ? measure(\"length\", \"length\") * measure(\"width\", \"length\") * measure(\"depth\", \"length\") : derivation?.method === \"area-depth\" ? measure(\"area\", \"area\") * measure(\"depth\", \"length\") : NaN\n    if (derivation?.version !== \"1\" || q.unitId !== \"m3\" || !close(q.value, expected)) errors.push(\"Calculated volume does not match the preserved inputs and method.\")\n  }\n  if (q.conversion !== undefined && !record(q.conversion)) errors.push(\"Quantity conversion must be an object.\")\n  if (record(q.conversion)) {\n    if (q.conversion.state === \"unavailable\") {\n      if (typeof q.conversion.reason !== \"string\" || !q.conversion.reason.trim()) errors.push(\"Unavailable conversions must include a reason.\")\n      return errors\n    }\n    if (q.conversion.state !== \"available\") {\n      errors.push(\"Quantity conversion state must be available or unavailable.\")\n      return errors\n    }\n    const c = q.conversion\n    const target = units.find(u => isQuantityUnit(u) && u.id === c.targetUnitId && [\"volume\", \"mass\"].includes(u.kind))\n    if (!target) return [...errors, \"Unknown conversion target unit.\"]\n    let factor = unit.factorToBase / target.factorToBase\n    if (unit.kind !== target.kind) {\n      const density = record(c.density) ? c.density : undefined\n      const densityValue = density && typeof density.value === \"number\" ? density.value : NaN\n      const commonDensityValid = density && typeof density.value === \"number\" && Number.isFinite(density.value) && density.value > 0 && density.unit === \"t/m3\" && typeof density.source === \"string\" && density.source.trim() && q.basis !== \"unknown\" && q.moistureBasis !== \"unknown\" && density.basis === q.basis && density.moistureBasis === q.moistureBasis && c.estimated === true\n      let densityValid = false\n      if (commonDensityValid && density.verification === \"not-assessed\" && !density.reference) {\n        densityValid = true\n      } else if (commonDensityValid && density.verification === \"reviewed-reference\" && reference) {\n        const selection = record(density.reference) ? density.reference : undefined\n        const candidate = selection && typeof selection.id === \"string\" && typeof selection.version === \"string\"\n          ? reference.densityReferences.find(row => row.id === selection.id && row.version === selection.version)\n          : undefined\n        const catalogueVersion = selection && typeof selection.catalogueVersion === \"string\" ? selection.catalogueVersion : \"\"\n        densityValid = Boolean(candidate && isReviewedDensityReference(candidate) && catalogueVersion === data.version && (!candidate.catalogueVersion || candidate.catalogueVersion === catalogueVersion) && candidate.classKey === material?.classKey && candidate.basis === density.basis && candidate.moistureBasis === density.moistureBasis && candidate.source === density.source && close(density.value, candidate.mid))\n      }\n      if (!densityValid) return [...errors, \"Density conversion needs an explicit source and compatible known physical and moisture bases.\"]\n      factor *= unit.kind === \"volume\" ? densityValue * 1000 : 1 / (densityValue * 1000)\n    }\n    if (q.value !== undefined ? !close(c.value, Number(q.value) * factor) || c.min !== undefined || c.max !== undefined : !close(c.min, Number(q.min) * factor) || !close(c.max, Number(q.max) * factor) || c.value !== undefined) errors.push(\"Converted values do not match the preserved source quantity.\")\n  }\n  return errors\n}\n",
      "type": "registry:component",
      "target": "components/blocks/domain/material-selection/material-selection-validation.ts"
    },
    {
      "path": "components/blocks/domain/material-selection/material-selection.tsx",
      "content": "\"use client\"\n\nimport { useId, useRef, useState } from \"react\"\nimport { ArrowDownLeft, ArrowUpRight, Edit03 } from \"@untitledui/icons\"\nimport { Button } from \"@/components/ui/button\"\nimport { Card, CardContent, CardHeader } from \"@/components/ui/card\"\nimport { Field, FieldContent, FieldDescription, FieldLabel, FieldTitle } from \"@/components/ui/field\"\nimport { RadioGroup, RadioGroupItem } from \"@/components/ui/radio-group\"\nimport { Separator } from \"@/components/ui/separator\"\nimport { Tabs, TabsContent, TabsList, TabsTrigger } from \"@/components/ui/tabs\"\nimport { cn } from \"@/lib/utils\"\nimport { MaterialArtwork } from \"./material-artwork\"\nimport { MaterialBrowser } from \"./material-browser\"\nimport { MaterialDetails } from \"./material-details\"\nimport { MaterialSelectionSummary } from \"./material-selection-summary\"\nimport { materialLabel, blankDraft, chooseMaterial, chooseSpecification, hasSelection, type Intent, type MaterialDraft, type MaterialOption, type MaterialReference, type MaterialVisuals } from \"./material-selection-model\"\n\n/** Material identity, requested/reported details and uncertainty. The host owns I/O. */\nexport function MaterialSelection({ data, visuals, value, onValueChange, onIntentChange, onConfirm, className }: {\n  data: MaterialReference; visuals?: MaterialVisuals; value: MaterialDraft;\n  onValueChange: (value: MaterialDraft) => void; onIntentChange?: (intent: Intent) => void;\n  onConfirm?: (value: MaterialDraft) => void; className?: string\n}) {\n  const id = useId()\n  const [editing, setEditing] = useState(false)\n  const heading = useRef<HTMLHeadingElement>(null)\n  const material = data.classes.find(c => c.key === value.material?.classKey)\n  const hasMaterial = hasSelection(value)\n  const selecting = !hasMaterial || editing\n  const choose = (option: MaterialOption) => {\n    onValueChange(chooseMaterial(value, option, data)); setEditing(false)\n    requestAnimationFrame(() => heading.current?.focus())\n  }\n  return <div className={cn(\"flex min-w-0 flex-col gap-6\", className)}>\n    {onIntentChange && <RadioGroup aria-label=\"Material direction\" value={value.intent} onValueChange={next => { if (next === \"need\" || next === \"have\") { onIntentChange(next); setEditing(false) } }} className=\"grid gap-4 sm:grid-cols-2\">\n      {([[\"need\", \"I need material\", \"Find material for a project or an upcoming job\", ArrowDownLeft], [\"have\", \"I have material\", \"Describe material available or coming off site\", ArrowUpRight]] as const).map(([intent, title, description, Icon]) => <FieldLabel key={intent} htmlFor={`${id}-${intent}`} className=\"cursor-pointer rounded-xl bg-card shadow-xs\">\n        <Field orientation=\"horizontal\" className=\"items-center p-4\"><span className=\"rounded-lg border bg-background p-2 shadow-xs\"><Icon className=\"size-5 text-muted-foreground\" /></span><FieldContent><FieldTitle className=\"font-semibold\">{title}</FieldTitle><FieldDescription>{description}</FieldDescription></FieldContent><RadioGroupItem id={`${id}-${intent}`} value={intent} /></Field>\n      </FieldLabel>)}\n    </RadioGroup>}\n    <div className=\"grid min-w-0 items-start gap-6 xl:grid-cols-[minmax(0,1fr)_340px]\">\n      <Card className=\"min-w-0\"><Tabs value={selecting ? \"material\" : \"details\"} onValueChange={next => setEditing(next === \"material\")} className=\"gap-6\"><CardHeader className=\"border-b\"><TabsList variant=\"line\" aria-label=\"Selection steps\"><TabsTrigger value=\"material\">1. Material</TabsTrigger><TabsTrigger value=\"details\" disabled={!hasMaterial}>2. Details <span className=\"font-normal text-muted-foreground\">(optional)</span></TabsTrigger></TabsList></CardHeader>\n        <CardContent><TabsContent value=\"material\"><MaterialBrowser data={data} visuals={visuals} intent={value.intent} onSelect={choose} onSelectSpecification={key => { onValueChange(chooseSpecification(value, key)); setEditing(false) }} onDescribe={description => { onValueChange({ ...blankDraft(value.intent), description }); setEditing(false); requestAnimationFrame(() => heading.current?.focus()) }} /></TabsContent><TabsContent value=\"details\"><div className=\"flex flex-col gap-6\">\n          <div className=\"flex items-center gap-4\">{material && <MaterialArtwork classKey={material.key} visuals={visuals} className=\"size-16\" />}<div className=\"min-w-0 flex-1\"><p className=\"text-sm text-muted-foreground\">{value.intent === \"need\" ? \"You’re looking for\" : \"Your material\"}</p><h2 ref={heading} tabIndex={-1} className=\"text-xl font-semibold outline-none\">{material ? materialLabel(data, material) : \"Material to identify\"}</h2>{!material && <p className=\"mt-1 text-sm text-muted-foreground\">{value.description}</p>}</div><Button variant=\"secondary\" size=\"sm\" onClick={() => setEditing(true)}><Edit03 />Change</Button></div>\n          <Separator /><MaterialDetails data={data} value={value} onChange={onValueChange} />\n        </div></TabsContent></CardContent>\n      </Tabs></Card>\n      <aside className={cn(\"min-w-0 xl:sticky xl:top-6\", !hasMaterial && \"hidden xl:block\")}><MaterialSelectionSummary data={data} visuals={visuals} value={value} onConfirm={onConfirm} /><p className=\"px-1 pt-3 text-xs text-muted-foreground\">Artwork is illustrative, not a material sample.</p></aside>\n    </div>\n  </div>\n}\n",
      "type": "registry:component",
      "target": "components/blocks/domain/material-selection/material-selection.tsx"
    }
  ],
  "type": "registry:block"
}