{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pad-inspector",
  "type": "registry:component",
  "title": "Pad inspector",
  "description": "Circuit-board pad selection with supplied footprint references, net and layer records, pad types and a dimension table with explicit units.",
  "registryDependencies": [
    "https://vlak.dev/r/vlak-base.json",
    "https://vlak.dev/r/inter.json",
    "https://vlak.dev/r/select.json",
    "https://vlak.dev/r/dropdown-menu.json",
    "https://vlak.dev/r/vlak-lib.json"
  ],
  "dependencies": [
    "@stylexjs/stylex"
  ],
  "devDependencies": [
    "@stylexjs/babel-plugin"
  ],
  "docs": "Vlak leaves are StyleX. Compile them with @stylexjs/babel-plugin (Vite: @stylexjs/unplugin, Next: @stylexjs/nextjs-plugin). If you would rather not run a compiler, import @noorddev/vlak-react instead: it ships precompiled with one stylesheet.",
  "files": [
    {
      "path": "vlak/pad-inspector.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as stylex from \"@stylexjs/stylex\";\nimport { vlak } from \"./tokens.stylex\";\nimport { rs } from \"./rs\";\nimport { Select } from \"./select\";\nimport { useInputValue } from \"./use-input-value\";\nimport { useMergedRefs } from \"./merge-refs\";\n\nexport interface PadInspectorMeasurement { id: string; label: string; value: number | string | null; unit: string | null }\nexport interface PadInspectorPad {\n  id: string;\n  number?: string | null;\n  reference?: string | null;\n  net?: string | null;\n  /** null or omitted means unknown; an empty list means none recorded. */\n  layers?: readonly string[] | null;\n  type?: string | null;\n  shape?: string | null;\n  geometry?: readonly PadInspectorMeasurement[];\n  disabled?: boolean;\n}\nexport interface PadInspectorProps extends Omit<React.FieldsetHTMLAttributes<HTMLFieldSetElement>, \"defaultValue\"> {\n  label: React.ReactNode;\n  pads: readonly PadInspectorPad[];\n  value?: string | null;\n  defaultValue?: string | null;\n  /** Requests pad selection only. The host owns board and schematic cross-highlighting. */\n  onValueChange?: (id: string | null) => void;\n  boardLabel?: React.ReactNode;\n  description?: React.ReactNode;\n  selectionLabel?: string;\n  required?: boolean;\n  readOnly?: boolean;\n}\n\nconst styles = stylex.create({\n  root: { display: \"grid\", gap: \"0.875rem\", minWidth: 0, margin: 0, padding: 0, borderWidth: 0, color: vlak.ink, lineHeight: 1.45 },\n  legend: { padding: 0, marginBottom: \"0.75rem\", fontSize: vlak.controlFs, fontWeight: 600 },\n  field: { display: \"grid\", gap: \"0.375rem\", minWidth: 0, fontSize: vlak.controlLabel },\n  control: { minWidth: 0, width: \"100%\", minHeight: vlak.hit },\n  properties: { display: \"grid\", gridTemplateColumns: \"repeat(auto-fit, minmax(min(100%, 8rem), 1fr))\", gap: \"0.875rem\", margin: 0, paddingBlock: \"0.875rem\", borderTopWidth: vlak.hairline, borderTopStyle: \"solid\", borderTopColor: vlak.divider },\n  term: { color: vlak.gray, fontSize: vlak.controlLabel },\n  detail: { margin: 0, fontSize: vlak.controlFs, overflowWrap: \"anywhere\" },\n  table: { width: \"100%\", borderCollapse: \"collapse\", tableLayout: \"fixed\", fontSize: vlak.controlLabel },\n  caption: { textAlign: \"start\", fontSize: vlak.controlFs, fontWeight: 600, paddingBottom: \"0.5rem\" },\n  cell: { textAlign: \"start\", verticalAlign: \"top\", paddingBlock: \"0.625rem\", paddingInlineEnd: \"0.5rem\", borderBottomWidth: vlak.hairline, borderBottomStyle: \"solid\", borderBottomColor: vlak.divider, fontWeight: 400, fontVariantNumeric: \"tabular-nums\", overflowWrap: \"anywhere\" },\n  note: { margin: 0, color: vlak.gray, fontSize: vlak.controlLabel, overflowWrap: \"anywhere\", maxWidth: \"66ch\" },\n});\n\nfunction text(value: string | null | undefined) { return value?.trim() ? value : \"Not supplied\"; }\nfunction measurement(value: number | string | null) { return value == null || typeof value === \"string\" && !value.trim() ? \"Not supplied\" : typeof value === \"number\" && !Number.isFinite(value) ? \"Unavailable\" : value; }\n\n/** Inspects supplied pad records without inferring connectivity, shape or dimensions. */\nexport const PadInspector = React.forwardRef<HTMLFieldSetElement, PadInspectorProps>(function PadInspector({ label, pads, value, defaultValue = null, onValueChange, boardLabel, description, selectionLabel = \"Pad\", required, readOnly, disabled, name, form, className, style, children, \"aria-describedby\": ariaDescribedBy, ...props }, ref) {\n  const id = React.useId();\n  const [, refresh] = React.useReducer((n: number) => n + 1, 0);\n  const [current, setValue, fieldRef] = useInputValue<string | null, HTMLFieldSetElement>(value, defaultValue, onValueChange, refresh);\n  const mergedRef = useMergedRefs(ref, fieldRef);\n  const validRecords = pads.every(pad => pad.id.length > 0 && (!pad.geometry || pad.geometry.every(item => item.id.length > 0) && new Set(pad.geometry.map(item => item.id)).size === pad.geometry.length)) && new Set(pads.map(pad => pad.id)).size === pads.length;\n  const selected = pads.find(pad => pad.id === current);\n  const unavailable = current != null && (!selected || selected.disabled);\n  const describedBy = [ariaDescribedBy, `${id}-selection`, description != null && `${id}-description`, readOnly && `${id}-readonly`].filter(Boolean).join(\" \");\n  const root = rs([\"rs-pad-inspector\", className], styles.root);\n  const legend = rs([\"rs-pad-inspector-legend\"], styles.legend);\n  const field = rs([\"rs-pad-inspector-label\"], styles.field);\n  const control = rs([\"rs-pad-inspector-control\"], styles.control);\n  const properties = rs([\"rs-pad-inspector-properties\"], styles.properties);\n  const term = rs([\"rs-pad-inspector-term\"], styles.term);\n  const detail = rs([\"rs-pad-inspector-detail\"], styles.detail);\n  const table = rs([\"rs-pad-inspector-table\"], styles.table);\n  const caption = rs([\"rs-pad-inspector-caption\"], styles.caption);\n  const cell = rs([\"rs-pad-inspector-cell\"], styles.cell);\n  const note = rs([\"rs-pad-inspector-note\"], styles.note);\n  const records = selected ? [[\"Pad identifier\", selected.id], [\"Footprint reference\", text(selected.reference)], [\"Pad number\", text(selected.number)], [\"Net\", text(selected.net)], [\"Pad type\", text(selected.type)], [\"Shape\", text(selected.shape)], [\"Layers\", selected.layers == null ? \"Not supplied\" : selected.layers.length === 0 ? \"None recorded\" : selected.layers.join(\", \")]] : [];\n  return <fieldset {...props} ref={mergedRef} name={name} form={form} disabled={disabled} aria-describedby={describedBy} className={root.className} style={{ ...root.style, ...style }}>\n    <legend {...legend}>{label}</legend>\n    {boardLabel != null && <p {...note}>{boardLabel}</p>}\n    {description != null && <p {...note} id={`${id}-description`}>{description}</p>}\n    {readOnly && <p {...note} id={`${id}-readonly`}>Read only</p>}\n    {!validRecords ? <p {...note}>Pad and measurement identifiers must be unique and non-empty.</p> : <>\n      <div {...field}><span id={`${id}-choice-label`}>{selectionLabel}</span><Select {...control} fullWidth name={name} form={form} value={unavailable ? \"\" : current ?? \"\"} disabled={disabled} readOnly={readOnly} required={required || unavailable} aria-invalid={unavailable || undefined} aria-labelledby={`${id}-choice-label`} aria-describedby={describedBy} options={[{ value: \"\", label: unavailable ? `Unavailable pad: ${current}` : \"No pad selected\" }, ...pads.map(pad => ({ value: pad.id, label: `${pad.reference?.trim() ? `${pad.reference} / ` : \"\"}${pad.number?.trim() ? `Pad ${pad.number} · ` : \"\"}${pad.id}`, disabled: pad.disabled }))]} onValueChange={next => { if (!readOnly && !disabled) setValue(next || null); }} /></div>\n      {selected && <>\n        <dl {...properties}>{records.map(([key, value]) => <div key={key}><dt {...term}>{key}</dt><dd {...detail}>{value}</dd></div>)}</dl>\n        {selected.geometry?.length ? <table {...table}><caption {...caption}>Supplied geometry</caption><thead><tr><th {...cell} scope=\"col\">Dimension</th><th {...cell} scope=\"col\">Value</th><th {...cell} scope=\"col\">Unit</th></tr></thead><tbody>{selected.geometry.map(item => <tr key={item.id}><th {...cell} scope=\"row\">{item.label}</th><td {...cell}>{measurement(item.value)}</td><td {...cell}>{text(item.unit)}</td></tr>)}</tbody></table> : <p {...note}>Geometry not supplied</p>}\n      </>}\n      {pads.length === 0 && <p {...note}>No pad records supplied</p>}\n    </>}\n    <p {...note} id={`${id}-selection`}>{unavailable ? `Selected pad unavailable: ${current}` : selected ? `Selected pad: ${selected.id}` : \"No pad selected\"}</p>\n    {children}\n  </fieldset>;\n});\n",
      "type": "registry:component",
      "target": "components/vlak/pad-inspector.tsx"
    },
    {
      "path": "vlak/use-input-value.ts",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\n/** Shared state for compound inputs. Native form reset restores uncontrolled defaults. */\nexport function useInputValue<T, E extends HTMLElement>(\n  value: T | undefined,\n  defaultValue: T,\n  onValueChange?: (value: T) => void,\n  onReset?: () => void,\n) {\n  const [inner, setInner] = React.useState(defaultValue);\n  const element = React.useRef<E>(null);\n  const controlled = value !== undefined;\n  const reset = React.useRef({ controlled, defaultValue, onReset });\n  reset.current = { controlled, defaultValue, onReset };\n  React.useEffect(() => {\n    const node = element.current;\n    const form = node && \"form\" in node ? (node as unknown as HTMLInputElement).form : node?.closest(\"form\");\n    if (!form) return;\n    const handleReset = (event: Event) => {\n      queueMicrotask(() => {\n        if (!event.defaultPrevented) {\n          if (!reset.current.controlled) setInner(reset.current.defaultValue);\n          reset.current.onReset?.();\n        }\n      });\n    };\n    form.addEventListener(\"reset\", handleReset);\n    return () => form.removeEventListener(\"reset\", handleReset);\n  }, []);\n  const setValue = (next: T) => {\n    if (!controlled) setInner(next);\n    onValueChange?.(next);\n  };\n  return [controlled ? value : inner, setValue, element] as const;\n}\n",
      "type": "registry:file",
      "target": "components/vlak/use-input-value.ts"
    },
    {
      "path": "vlak/styles/pad-inspector.css",
      "content": "/* ── pad-inspector: generated from packages/react/src/components/pad-inspector.tsx ── */\n.rs-pad-inspector{display:grid;gap:0.875rem;min-width:0;margin:0;padding:0;border-width:0;color:var(--text);line-height:1.45}\n.rs-pad-inspector-legend{padding:0;margin-bottom:0.75rem;font-size:var(--control-fs);font-weight:600}\n.rs-pad-inspector-label{display:grid;gap:0.375rem;min-width:0;font-size:var(--control-label)}\n.rs-pad-inspector-control{min-width:0;width:100%;min-height:var(--hit)}\n.rs-pad-inspector-properties{display:grid;grid-template-columns:repeat(auto-fit, minmax(min(100%, 8rem), 1fr));gap:0.875rem;margin:0;padding-block:0.875rem;border-top-width:1px;border-top-style:solid;border-top-color:var(--divider)}\n.rs-pad-inspector-term{color:var(--text-secondary);font-size:var(--control-label)}\n.rs-pad-inspector-detail{margin:0;font-size:var(--control-fs);overflow-wrap:anywhere}\n.rs-pad-inspector-table{width:100%;border-collapse:collapse;table-layout:fixed;font-size:var(--control-label)}\n.rs-pad-inspector-caption{text-align:start;font-size:var(--control-fs);font-weight:600;padding-bottom:0.5rem}\n.rs-pad-inspector-cell{text-align:start;vertical-align:top;padding-block:0.625rem;padding-inline-end:0.5rem;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:var(--divider);font-weight:400;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}\n.rs-pad-inspector-note{margin:0;color:var(--text-secondary);font-size:var(--control-label);overflow-wrap:anywhere;max-width:66ch}\n",
      "type": "registry:file",
      "target": "styles/vlak/pad-inspector.css"
    }
  ],
  "meta": {
    "vlak": {
      "category": "electronics",
      "classes": [
        "rs-pad-inspector",
        "rs-pad-inspector-legend",
        "rs-pad-inspector-label",
        "rs-pad-inspector-control",
        "rs-pad-inspector-properties",
        "rs-pad-inspector-term",
        "rs-pad-inspector-detail",
        "rs-pad-inspector-table",
        "rs-pad-inspector-caption",
        "rs-pad-inspector-cell",
        "rs-pad-inspector-note"
      ],
      "snippet": "<fieldset class=\"rs-pad-inspector\"><legend class=\"rs-pad-inspector-legend\">Connector pads</legend><div class=\"rs-pad-inspector-label\"><span id=\"pad-choice-label\">Pad</span><div class=\"rs-select rs-select-fluid rs-pad-inspector-control\"><button type=\"button\" class=\"rs-dropdown\" role=\"combobox\" aria-labelledby=\"pad-choice-label\" aria-expanded=\"false\" aria-haspopup=\"listbox\">J1 / Pad 1 · pad-a</button></div></div><dl class=\"rs-pad-inspector-properties\"><div><dt class=\"rs-pad-inspector-term\">Net</dt><dd class=\"rs-pad-inspector-detail\">Supply</dd></div><div><dt class=\"rs-pad-inspector-term\">Layers</dt><dd class=\"rs-pad-inspector-detail\">Front copper, Back copper</dd></div></dl><table class=\"rs-pad-inspector-table\"><caption class=\"rs-pad-inspector-caption\">Supplied geometry</caption><thead><tr><th class=\"rs-pad-inspector-cell\" scope=\"col\">Dimension</th><th class=\"rs-pad-inspector-cell\" scope=\"col\">Value</th><th class=\"rs-pad-inspector-cell\" scope=\"col\">Unit</th></tr></thead><tbody><tr><th class=\"rs-pad-inspector-cell\" scope=\"row\">Pad diameter</th><td class=\"rs-pad-inspector-cell\">1.80</td><td class=\"rs-pad-inspector-cell\">mm</td></tr></tbody></table></fieldset>",
      "cssOnly": false,
      "registryDependencies": [
        "select",
        "dropdown-menu",
        "vlak-lib"
      ],
      "aliases": [
        "Footprint pad",
        "Circuit board pad properties",
        "Pad geometry",
        "Copper pad inspector",
        "Board pad selector"
      ],
      "example": "import { PadInspector } from \"@noorddev/vlak-react\";\n\n<PadInspector label=\"Connector pads\" name=\"pad\" boardLabel=\"Example board, revision C\" defaultValue=\"pad-a\" pads={[\n  { id: \"pad-a\", reference: \"J1\", number: \"1\", net: \"Supply\", layers: [\"Front copper\", \"Back copper\"], type: \"Through hole\", shape: \"Circle\", geometry: [\n    { id: \"diameter\", label: \"Pad diameter\", value: \"1.80\", unit: \"mm\" },\n    { id: \"drill\", label: \"Drill diameter\", value: \"0.90\", unit: \"mm\" },\n  ] },\n  { id: \"pad-b\", reference: \"J1\", number: \"2\", net: \"Return\", layers: [\"Front copper\"], type: \"Surface mount\", shape: \"Rectangle\" },\n]} />",
      "usage": {
        "use": [
          "Inspecting a bounded collection of pad records supplied by a board editor or manufacturing export.",
          "Supply unique, non-empty pad ids and unique measurement ids within each pad. Pad numbers may repeat; ids identify the actual records.",
          "Use value and onValueChange for host-owned selection and board or schematic cross-highlighting. No connectivity or geometry calculation runs in the component.",
          "Supply geometry values as strings to preserve significant figures. Null fields remain explicitly missing; an empty layers array means none recorded."
        ],
        "avoid": [
          "Inferring that a missing net means unconnected, a missing drill means no hole, or an unknown layer list means every copper layer.",
          "Treating selection as a board edit or inferring dimensions from the shape name."
        ]
      },
      "keyboard": [
        {
          "keys": "Tab",
          "does": "Focuses the Vlak pad selector."
        },
        {
          "keys": "Arrow keys, Home, End",
          "does": "Opens or moves through available pad choices."
        },
        {
          "keys": "Enter, Space",
          "does": "Chooses the focused pad. Controlled selection waits for a host update."
        },
        {
          "keys": "Escape",
          "does": "Closes the selector and keeps the current choice."
        }
      ],
      "a11y": [
        "A fieldset legend names the inspector. Pad choices include their record id, so repeated pad numbers remain distinguishable.",
        "Vlak Select supplies the 44px trigger, keyboard listbox and disabled choices. Pad properties use a description list and dimensions use a captioned table with row and column headers.",
        "Missing records, unavailable selections, non-finite measurements and missing units have explicit text. Numeric zero and string precision are retained.",
        "Native form association and reset are supported. Read-only valid selection remains submittable, while disabled selection is omitted.",
        "The fieldset ref, native attributes, className and style pass through."
      ]
    }
  }
}
