{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "datum-transform-picker",
  "type": "registry:component",
  "title": "Datum transform picker",
  "description": "A Vlak transformation selector with source and destination references, supplied accuracy and area, and visible support-grid availability.",
  "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/datum-transform-picker.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport * as stylex from \"@stylexjs/stylex\";\nimport { vlak, mq } 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 DatumTransformGrid { id: string; label: string; available: boolean | null }\nexport interface DatumTransformation {\n  id: string;\n  label: string;\n  accuracy?: React.ReactNode;\n  area?: React.ReactNode;\n  /** The host determines whether this operation can be used. Null means unknown. */\n  available: boolean | null;\n  unavailableReason?: React.ReactNode;\n  /** An empty array explicitly means no support grids are required. */\n  grids?: readonly DatumTransformGrid[];\n}\nexport interface DatumTransformPickerProps extends Omit<React.FieldsetHTMLAttributes<HTMLFieldSetElement>, \"defaultValue\"> {\n  label: React.ReactNode;\n  sourceReference: React.ReactNode;\n  destinationReference: React.ReactNode;\n  transformations: readonly DatumTransformation[];\n  /** null means no operation selected; an unknown identifier stays explicitly unavailable. */\n  value?: string | null;\n  defaultValue?: string | null;\n  onValueChange?: (value: string | null) => void;\n  description?: React.ReactNode;\n  selectionLabel?: string;\n  noneLabel?: 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  context: { display: \"grid\", gridTemplateColumns: \"repeat(auto-fit, minmax(min(100%, 10rem), 1fr))\", gap: \"0.75rem\", margin: 0 },\n  term: { fontSize: vlak.controlLabel, color: vlak.gray },\n  detail: { margin: 0, fontSize: vlak.controlFs, overflowWrap: \"anywhere\" },\n  field: { display: \"grid\", gap: \"0.375rem\", fontSize: vlak.controlLabel },\n  control: { minWidth: 0, width: \"100%\", minHeight: vlak.hit },\n  list: { listStyle: \"none\", margin: 0, padding: 0, display: \"grid\", gap: \"0.5rem\" },\n  item: { display: \"grid\", gap: \"0.625rem\", padding: \"0.875rem\", borderWidth: vlak.hairline, borderStyle: \"solid\", borderColor: vlak.divider, minWidth: 0 },\n  selected: { backgroundColor: vlak.tableAlt, borderColor: { default: vlak.controlBorder, [mq.forcedColors]: \"Highlight\" } },\n  title: { margin: 0, fontWeight: 600, fontSize: vlak.controlFs, overflowWrap: \"anywhere\" },\n  note: { margin: 0, fontSize: vlak.controlLabel, color: vlak.gray, overflowWrap: \"anywhere\", maxWidth: \"66ch\" },\n});\n\nfunction canUse(item: DatumTransformation) { return item.available === true && !item.grids?.some(grid => grid.available !== true); }\nfunction availability(item: DatumTransformation) { return item.available == null ? \"Availability unknown\" : item.available === false ? \"Unavailable\" : item.grids?.some(grid => grid.available !== true) ? \"Required grids unavailable\" : \"Available\"; }\n\n/** Compares host-supplied operations and keeps unavailable alternatives visible. */\nexport const DatumTransformPicker = React.forwardRef<HTMLFieldSetElement, DatumTransformPickerProps>(function DatumTransformPicker({ label, sourceReference, destinationReference, transformations, value, defaultValue = null, onValueChange, description, selectionLabel = \"Transformation\", noneLabel = \"No transformation selected\", 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 validOptions = transformations.every(item => item.id.length > 0) && new Set(transformations.map(item => item.id)).size === transformations.length;\n  const selected = transformations.find(item => item.id === current);\n  const invalid = current != null && (!selected || !canUse(selected));\n  const describedBy = [ariaDescribedBy, `${id}-selection`, description != null && `${id}-description`, readOnly && `${id}-readonly`].filter(Boolean).join(\" \");\n  const root = rs([\"rs-datum-transform-picker\", className], styles.root);\n  const legend = rs([\"rs-datum-transform-picker-legend\"], styles.legend);\n  const context = rs([\"rs-datum-transform-picker-context\"], styles.context);\n  const term = rs([\"rs-datum-transform-picker-term\"], styles.term);\n  const detail = rs([\"rs-datum-transform-picker-detail\"], styles.detail);\n  const field = rs([\"rs-datum-transform-picker-label\"], styles.field);\n  const control = rs([\"rs-datum-transform-picker-control\"], styles.control);\n  const list = rs([\"rs-datum-transform-picker-list\"], styles.list);\n  const title = rs([\"rs-datum-transform-picker-title\"], styles.title);\n  const note = rs([\"rs-datum-transform-picker-note\"], styles.note);\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    {description != null && <p {...note} id={`${id}-description`}>{description}</p>}\n    {readOnly && <p {...note} id={`${id}-readonly`}>Read only</p>}\n    <dl {...context}><div><dt {...term}>Source reference</dt><dd {...detail}>{sourceReference ?? \"Unknown source reference\"}</dd></div><div><dt {...term}>Destination reference</dt><dd {...detail}>{destinationReference ?? \"Unknown destination reference\"}</dd></div></dl>\n    {!validOptions ? <p {...note}>Transformation 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={invalid ? \"\" : current ?? \"\"} disabled={disabled} readOnly={readOnly} required={required || invalid} aria-invalid={invalid || undefined} aria-labelledby={`${id}-choice-label`} aria-describedby={describedBy} options={[{ value: \"\", label: invalid ? `Unavailable transformation: ${current}` : noneLabel }, ...transformations.map(item => ({ value: item.id, label: `${item.label}${canUse(item) ? \"\" : ` (${availability(item)})`}`, disabled: !canUse(item) }))]} onValueChange={next => { if (!readOnly && !disabled) setValue(next || null); }} /></div>\n      <ul {...list}>{transformations.map(item => {\n        const row = rs([\"rs-datum-transform-picker-item\", current === item.id && \"rs-datum-transform-picker-selected\"], styles.item, current === item.id && styles.selected);\n        return <li {...row} key={item.id}><p {...title}>{item.label}{current === item.id ? \" · Selected\" : \"\"}</p><p {...note}>{availability(item)}{!canUse(item) && item.unavailableReason != null ? <>: {item.unavailableReason}</> : null}</p><dl {...context}><div><dt {...term}>Reported accuracy</dt><dd {...detail}>{item.accuracy ?? \"Not supplied\"}</dd></div><div><dt {...term}>Area of use</dt><dd {...detail}>{item.area ?? \"Not supplied\"}</dd></div></dl><p {...note}>Support grids: {item.grids == null ? \"Requirements not supplied\" : item.grids.length === 0 ? \"None required\" : item.grids.map(grid => `${grid.label} (${grid.available === true ? \"available\" : grid.available === false ? \"missing\" : \"availability unknown\"})`).join(\", \")}</p></li>;\n      })}</ul>\n      {transformations.length === 0 && <p {...note}>No transformations supplied</p>}\n    </>}\n    <p {...note} id={`${id}-selection`}>{current == null ? noneLabel : !selected ? `Unknown transformation: ${current}` : invalid ? `Selected transformation unavailable: ${selected.label}` : `Selected: ${selected.label}`}</p>\n    {children}\n  </fieldset>;\n});\n",
      "type": "registry:component",
      "target": "components/vlak/datum-transform-picker.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/datum-transform-picker.css",
      "content": "/* ── datum-transform-picker: generated from packages/react/src/components/datum-transform-picker.tsx ── */\n.rs-datum-transform-picker{display:grid;gap:0.875rem;min-width:0;margin:0;padding:0;border-width:0;color:var(--text);line-height:1.45}\n.rs-datum-transform-picker-legend{padding:0;margin-bottom:0.75rem;font-size:var(--control-fs);font-weight:600}\n.rs-datum-transform-picker-context{display:grid;grid-template-columns:repeat(auto-fit, minmax(min(100%, 10rem), 1fr));gap:0.75rem;margin:0}\n.rs-datum-transform-picker-term{font-size:var(--control-label);color:var(--text-secondary)}\n.rs-datum-transform-picker-detail{margin:0;font-size:var(--control-fs);overflow-wrap:anywhere}\n.rs-datum-transform-picker-label{display:grid;gap:0.375rem;font-size:var(--control-label)}\n.rs-datum-transform-picker-control{min-width:0;width:100%;min-height:var(--hit)}\n.rs-datum-transform-picker-list{list-style:none;margin:0;padding:0;display:grid;gap:0.5rem}\n.rs-datum-transform-picker-item{display:grid;gap:0.625rem;padding:0.875rem;border-width:1px;border-style:solid;border-color:var(--divider);min-width:0}\n.rs-datum-transform-picker-selected{background-color:var(--table-alt);border-color:var(--control-border)}\n@media (forced-colors: active){.rs-datum-transform-picker-selected{border-color:Highlight}}\n.rs-datum-transform-picker-title{margin:0;font-weight:600;font-size:var(--control-fs);overflow-wrap:anywhere}\n.rs-datum-transform-picker-note{margin:0;font-size:var(--control-label);color:var(--text-secondary);overflow-wrap:anywhere;max-width:66ch}\n",
      "type": "registry:file",
      "target": "styles/vlak/datum-transform-picker.css"
    }
  ],
  "meta": {
    "vlak": {
      "category": "geospatial",
      "classes": [
        "rs-datum-transform-picker",
        "rs-datum-transform-picker-legend",
        "rs-datum-transform-picker-context",
        "rs-datum-transform-picker-term",
        "rs-datum-transform-picker-detail",
        "rs-datum-transform-picker-label",
        "rs-datum-transform-picker-control",
        "rs-datum-transform-picker-list",
        "rs-datum-transform-picker-item",
        "rs-datum-transform-picker-selected",
        "rs-datum-transform-picker-title",
        "rs-datum-transform-picker-note"
      ],
      "snippet": "<fieldset class=\"rs-datum-transform-picker\"><legend class=\"rs-datum-transform-picker-legend\">Coordinate operation</legend><dl class=\"rs-datum-transform-picker-context\"><div><dt class=\"rs-datum-transform-picker-term\">Source reference</dt><dd class=\"rs-datum-transform-picker-detail\">Site grid</dd></div><div><dt class=\"rs-datum-transform-picker-term\">Destination reference</dt><dd class=\"rs-datum-transform-picker-detail\">Project grid</dd></div></dl><div class=\"rs-datum-transform-picker-label\"><span id=\"geo-transformation-label\">Transformation</span><div class=\"rs-select rs-select-fluid rs-datum-transform-picker-control\"><button type=\"button\" class=\"rs-dropdown\" role=\"combobox\" aria-labelledby=\"geo-transformation-label\" aria-expanded=\"false\" aria-haspopup=\"listbox\">Parameter transform</button></div></div><ul class=\"rs-datum-transform-picker-list\"><li class=\"rs-datum-transform-picker-item rs-datum-transform-picker-selected\"><p class=\"rs-datum-transform-picker-title\">Parameter transform · Selected</p><p class=\"rs-datum-transform-picker-note\">Available</p><dl class=\"rs-datum-transform-picker-context\"><div><dt class=\"rs-datum-transform-picker-term\">Reported accuracy</dt><dd class=\"rs-datum-transform-picker-detail\">1.5 m</dd></div><div><dt class=\"rs-datum-transform-picker-term\">Area of use</dt><dd class=\"rs-datum-transform-picker-detail\">Project extent</dd></div></dl><p class=\"rs-datum-transform-picker-note\">Support grids: None required</p></li></ul></fieldset>",
      "cssOnly": false,
      "registryDependencies": [
        "select",
        "dropdown-menu",
        "vlak-lib"
      ],
      "aliases": [
        "Datum transformation",
        "Coordinate operation",
        "Projection operation picker",
        "Support grid selector",
        "Reprojection choice"
      ],
      "example": "import { DatumTransformPicker } from \"@noorddev/vlak-react\";\n\n<DatumTransformPicker\n  label=\"Coordinate operation\"\n  sourceReference=\"Site grid\"\n  destinationReference=\"Project grid\"\n  name=\"operation\"\n  defaultValue=\"parameter\"\n  transformations={[\n    { id: \"grid\", label: \"Grid correction\", available: true, accuracy: \"0.05 m\", area: \"Site survey extent\", grids: [{ id: \"site\", label: \"Site correction grid\", available: false }], unavailableReason: \"Attach the supplied grid\" },\n    { id: \"parameter\", label: \"Parameter transform\", available: true, accuracy: \"1.5 m\", area: \"Project extent\", grids: [] },\n  ]}\n/>",
      "usage": {
        "use": [
          "Comparing operations supplied by a projection engine or survey provider before the application transforms coordinates.",
          "Supply unique, non-empty operation identifiers and explicit available values. false or null disables an operation; listed grids must all be available.",
          "Use an empty grids array to state that no support grids are required. Omit grids when the requirements were not supplied.",
          "null means no operation selected. Unknown current identifiers stay visible as unavailable. The application owns candidate generation, accuracy, compatibility and persistence."
        ],
        "avoid": [
          "Automatically picking the smallest reported error, assuming an unlisted grid is installed or silently falling back to a different operation.",
          "Presenting illustrative accuracy or area labels as calculated guarantees."
        ]
      },
      "keyboard": [
        {
          "keys": "Tab",
          "does": "Focuses the Vlak transformation selector."
        },
        {
          "keys": "Arrow keys, Home, End",
          "does": "Navigates the Vlak selector. Enter or Space chooses an available operation or the no-selection option; Escape closes the menu. Unavailable alternatives are skipped."
        }
      ],
      "a11y": [
        "Source and destination references have visible labels; null references are explicitly unknown.",
        "Every candidate retains visible accuracy, area, grid requirements and availability text, including disabled alternatives.",
        "The selected candidate uses a full-surface fill and a visible Selected label; forced colors retain a distinct border.",
        "A currently unavailable operation is marked invalid and fails native validation. Required selection uses the native required attribute.",
        "Vlak Select supplies the 44px trigger, menu, disabled choices and native form value. Read-only valid selections submit through a hidden value; disabled selections do not submit.",
        "Uncontrolled state follows native form reset, including external forms. Controlled state, native fieldset attributes, ref and consumer styles remain with the caller."
      ]
    }
  }
}
