{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "acquisition-sequencer",
  "type": "registry:component",
  "title": "Acquisition sequencer",
  "description": "Edits supplied capture steps, exact exposure, time, depth and channel values, and distinguishes requested actions from recorded run state.",
  "registryDependencies": [
    "https://vlak.dev/r/vlak-base.json",
    "https://vlak.dev/r/inter.json",
    "https://vlak.dev/r/button.json",
    "https://vlak.dev/r/input.json",
    "https://vlak.dev/r/select.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/acquisition-sequencer.tsx",
      "content": "\"use client\";\nimport * as React from \"react\";\nimport * as stylex from \"@stylexjs/stylex\";\nimport { vlak, mq } from \"./tokens.stylex\";\nimport { rs } from \"./rs\";\nimport { useInputValue } from \"./use-input-value\";\nimport { useMergedRefs } from \"./merge-refs\";\nimport { Button } from \"./button\";\nimport { Input } from \"./input\";\nimport { Select } from \"./select\";\n\nexport interface AcquisitionChannel { id: string; label: string; disabled?: boolean }\nexport interface AcquisitionStepAction { id: string; label: string; disabled?: boolean }\nexport interface AcquisitionStep {\n  id: string;\n  label: string;\n  exposure: number | null;\n  exposureUnit: string;\n  time: number | null;\n  timeUnit: string;\n  depth: number | null;\n  depthUnit: string;\n  channel: string | null;\n  /** Actual recorded state supplied by the host, never inferred from edits or requests. */\n  status: string;\n  confirmedLabel?: string;\n  pending?: boolean;\n  actions?: readonly AcquisitionStepAction[];\n}\nexport interface AcquisitionSequencerProps extends React.FieldsetHTMLAttributes<HTMLFieldSetElement> {\n  label: React.ReactNode;\n  steps: readonly AcquisitionStep[];\n  channels: readonly AcquisitionChannel[];\n  onStepsChange?: (steps: readonly AcquisitionStep[]) => void;\n  onAction?: (stepId: string, actionId: string) => void;\n  description?: React.ReactNode;\n  readOnly?: boolean;\n}\nconst styles = stylex.create({\n  root: { display: \"grid\", gap: \"0.75rem\", minWidth: 0, borderWidth: 0, padding: 0, margin: 0, color: vlak.ink, lineHeight: 1.45 },\n  legend: { padding: 0, marginBottom: \"0.75rem\", fontSize: \"0.875rem\", fontWeight: 600 },\n  list: { fontSize: \"0.875rem\", listStyleType: \"decimal\", paddingInlineStart: \"1.5rem\", margin: 0 },\n  step: { paddingBlock: \"1rem\", paddingInlineStart: \"0.25rem\", borderBottomWidth: vlak.hairline, borderBottomStyle: \"solid\", borderBottomColor: vlak.divider },\n  head: { display: \"flex\", flexWrap: \"wrap\", justifyContent: \"space-between\", alignItems: \"baseline\", gap: \"0.5rem\", marginBottom: \"0.75rem\" },\n  title: { fontSize: \"0.875rem\", fontWeight: 500 },\n  fields: { display: \"grid\", gridTemplateColumns: { default: \"repeat(4, minmax(0, 1fr))\", [mq.phone]: \"repeat(2, minmax(0, 1fr))\" }, gap: \"0.75rem\", margin: 0 },\n  field: { display: \"grid\", gap: \"0.375rem\", minWidth: 0, fontSize: \"0.75rem\" },\n  control: { minWidth: 0, minHeight: vlak.hit },\n  value: { margin: 0, fontSize: \"0.875rem\", fontVariantNumeric: \"tabular-nums\", overflowWrap: \"anywhere\" },\n  copy: { margin: 0, fontSize: \"0.75rem\", color: vlak.gray, maxWidth: \"66ch\", overflowWrap: \"anywhere\" },\n  actions: { display: \"flex\", flexWrap: \"wrap\", gap: \"0.5rem\", marginTop: \"0.75rem\" },\n  action: { minWidth: vlak.hit, minHeight: vlak.hit, width: \"auto\" },\n});\n/** A controlled capture plan editor and recorded-state display, without an acquisition engine. */\nexport const AcquisitionSequencer = React.forwardRef<HTMLFieldSetElement, AcquisitionSequencerProps>(function AcquisitionSequencer({ label, steps, channels, onStepsChange, onAction, description, readOnly = false, name, form, disabled, className, style, children, ...props }, ref) {\n  const [, refresh] = React.useReducer((n: number) => n + 1, 0);\n  const [, , fieldRef] = useInputValue<readonly AcquisitionStep[], HTMLFieldSetElement>(steps, steps, onStepsChange, refresh);\n  const merged = useMergedRefs(ref, fieldRef);\n  const id = React.useId();\n  const valid = steps.length <= 128 && channels.length <= 128 && steps.every(step => step.id.trim()) && channels.every(channel => channel.id.trim()) && new Set(steps.map(step => step.id)).size === steps.length && new Set(channels.map(channel => channel.id)).size === channels.length;\n  const update = (stepId: string, patch: Partial<AcquisitionStep>) => onStepsChange?.(steps.map(step => step.id === stepId ? { ...step, ...patch } : step));\n  const move = (index: number, delta: number) => { const next = [...steps]; [next[index], next[index + delta]] = [next[index + delta]!, next[index]!]; onStepsChange?.(next); };\n  const root = rs([\"rs-acquisition-sequencer\", className], styles.root);\n  const legend = rs([\"rs-acquisition-sequencer-legend\"], styles.legend);\n  const list = rs([\"rs-acquisition-sequencer-list\"], styles.list);\n  const stepStyle = rs([\"rs-acquisition-sequencer-step\"], styles.step);\n  const head = rs([\"rs-acquisition-sequencer-head\"], styles.head);\n  const title = rs([\"rs-acquisition-sequencer-title\"], styles.title);\n  const fields = rs([\"rs-acquisition-sequencer-fields\"], styles.fields);\n  const field = rs([\"rs-acquisition-sequencer-label\"], styles.field);\n  const control = rs([\"rs-acquisition-sequencer-control\"], styles.control);\n  const valueStyle = rs([\"rs-acquisition-sequencer-value\"], styles.value);\n  const copy = rs([\"rs-acquisition-sequencer-copy\"], styles.copy);\n  const actions = rs([\"rs-acquisition-sequencer-actions\"], styles.actions);\n  const action = rs([\"rs-acquisition-sequencer-action\"], styles.action);\n  return <fieldset {...props} ref={merged} disabled={disabled} name={name} form={form} className={root.className} style={{ ...root.style, ...style }}>\n    <legend {...legend}>{label}</legend>{description != null && <div {...copy}>{description}</div>}\n    {!valid ? <p {...copy}>Sequence unavailable: supply unique records and at most 128 steps and channels</p> : !steps.length ? <p {...copy}>No capture steps supplied</p> : <ol {...list}>{steps.map((step, index) => {\n      const editing = onStepsChange != null && !readOnly;\n      const locked = disabled || step.pending;\n      const statusId = `${id}-${index}-status`;\n      const labels = { exposure: \"Exposure\", time: \"Time offset\", depth: \"Depth\" };\n      return <li key={step.id} {...stepStyle}>\n        <div {...head}><span {...title}>{step.label}</span><span {...copy} id={statusId}>{step.status.trim() || \"Recorded state unavailable\"}</span></div>\n        <div {...fields}>{([\"exposure\", \"time\", \"depth\"] as const).map(key => {\n          const unit = step[`${key}Unit`];\n          const amount = step[key];\n          const available = amount != null && Number.isFinite(amount);\n          return editing ? <label key={key} {...field}>{labels[key]} ({unit})<Input {...control} plain type=\"number\" step=\"any\" min={key === \"depth\" ? undefined : 0} disabled={locked} name={name ? `${name}.${step.id}.${key}` : undefined} form={form} value={available ? amount : \"\"} aria-label={`${labels[key]} (${unit}): step ${index + 1}, ${step.label}`} aria-describedby={statusId} onChange={event => update(step.id, { [key]: Number.isFinite(event.currentTarget.valueAsNumber) ? event.currentTarget.valueAsNumber : null })} /></label> : <div key={key} {...field}><span>{labels[key]}</span><p {...valueStyle}>{available ? `${amount} ${unit}` : amount == null ? \"Not supplied\" : \"Unavailable\"}</p></div>;\n        })}\n          {editing ? <div {...field}><span>Channel</span><Select fullWidth disabled={locked} name={name ? `${name}.${step.id}.channel` : undefined} form={form} value={channels.some(channel => channel.id === step.channel) ? step.channel! : \"\"} aria-label={`Channel: step ${index + 1}, ${step.label}`} aria-describedby={statusId} options={[{ value: \"\", label: \"Choose channel\" }, ...channels.map(channel => ({ value: channel.id, label: channel.label, disabled: channel.disabled }))]} onValueChange={channel => update(step.id, { channel: channel || null })} /></div> : <div {...field}><span>Channel</span><p {...valueStyle}>{channels.find(channel => channel.id === step.channel)?.label ?? step.channel ?? \"Not supplied\"}</p></div>}\n        </div>\n        {step.confirmedLabel != null && <p {...copy}>{step.confirmedLabel}</p>}<span {...copy} role=\"status\">{step.pending ? \"Request pending; recorded state unchanged\" : null}</span>\n        <div {...actions}>{editing && <><Button {...action} variant=\"ghost\" disabled={locked || index === 0 || steps[index - 1]?.pending} aria-label={`Move step ${index + 1}, ${step.label}, up`} onClick={() => move(index, -1)}>Move up</Button><Button {...action} variant=\"ghost\" disabled={locked || index === steps.length - 1 || steps[index + 1]?.pending} aria-label={`Move step ${index + 1}, ${step.label}, down`} onClick={() => move(index, 1)}>Move down</Button></>}\n          {onAction && !readOnly && step.actions?.map(choice => <Button key={choice.id} {...action} variant=\"ghost\" disabled={locked || choice.disabled} aria-label={`${choice.label}: step ${index + 1}, ${step.label}`} aria-describedby={statusId} onClick={() => onAction(step.id, choice.id)}>{choice.label}</Button>)}\n        </div>\n      </li>;\n    })}</ol>}{children}\n  </fieldset>;\n});\n",
      "type": "registry:component",
      "target": "components/vlak/acquisition-sequencer.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/acquisition-sequencer.css",
      "content": "/* ── acquisition-sequencer: generated from packages/react/src/components/acquisition-sequencer.tsx ── */\n.rs-acquisition-sequencer{display:grid;gap:0.75rem;min-width:0;border-width:0;padding:0;margin:0;color:var(--text);line-height:1.45}\n.rs-acquisition-sequencer-legend{padding:0;margin-bottom:0.75rem;font-size:0.875rem;font-weight:600}\n.rs-acquisition-sequencer-list{font-size:0.875rem;list-style-type:decimal;padding-inline-start:1.5rem;margin:0}\n.rs-acquisition-sequencer-step{padding-block:1rem;padding-inline-start:0.25rem;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:var(--divider)}\n.rs-acquisition-sequencer-head{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:baseline;gap:0.5rem;margin-bottom:0.75rem}\n.rs-acquisition-sequencer-title{font-size:0.875rem;font-weight:500}\n.rs-acquisition-sequencer-fields{display:grid;grid-template-columns:repeat(4, minmax(0, 1fr));gap:0.75rem;margin:0}\n@media (max-width: 640px){.rs-acquisition-sequencer-fields{grid-template-columns:repeat(2, minmax(0, 1fr))}}\n.rs-acquisition-sequencer-label{display:grid;gap:0.375rem;min-width:0;font-size:0.75rem}\n.rs-acquisition-sequencer-control{min-width:0;min-height:var(--hit)}\n.rs-acquisition-sequencer-value{margin:0;font-size:0.875rem;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}\n.rs-acquisition-sequencer-copy{margin:0;font-size:0.75rem;color:var(--text-secondary);max-width:66ch;overflow-wrap:anywhere}\n.rs-acquisition-sequencer-actions{display:flex;flex-wrap:wrap;gap:0.5rem;margin-top:0.75rem}\n.rs-acquisition-sequencer-action{min-width:var(--hit);min-height:var(--hit);width:auto}\n",
      "type": "registry:file",
      "target": "styles/vlak/acquisition-sequencer.css"
    }
  ],
  "meta": {
    "vlak": {
      "category": "science",
      "classes": [
        "rs-acquisition-sequencer",
        "rs-acquisition-sequencer-legend",
        "rs-acquisition-sequencer-list",
        "rs-acquisition-sequencer-step",
        "rs-acquisition-sequencer-head",
        "rs-acquisition-sequencer-title",
        "rs-acquisition-sequencer-fields",
        "rs-acquisition-sequencer-label",
        "rs-acquisition-sequencer-control",
        "rs-acquisition-sequencer-value",
        "rs-acquisition-sequencer-copy",
        "rs-acquisition-sequencer-actions",
        "rs-acquisition-sequencer-action"
      ],
      "snippet": "<fieldset class=\"rs-acquisition-sequencer\"><legend class=\"rs-acquisition-sequencer-legend\">Capture plan</legend><ol class=\"rs-acquisition-sequencer-list\"><li class=\"rs-acquisition-sequencer-step\"><div class=\"rs-acquisition-sequencer-head\"><span class=\"rs-acquisition-sequencer-title\">First plane</span><span class=\"rs-acquisition-sequencer-copy\">Not run</span></div><p class=\"rs-acquisition-sequencer-value\">Exposure 10 ms · Time offset 0 s · Depth -1 µm · Phase</p></li></ol></fieldset>",
      "cssOnly": false,
      "registryDependencies": [
        "button",
        "input",
        "select",
        "vlak-lib"
      ],
      "aliases": [
        "Capture sequence",
        "Microscopy acquisition plan",
        "Time-lapse plan",
        "Capture step editor",
        "Instrument run plan"
      ],
      "example": "import { useState } from \"react\";\nimport { AcquisitionSequencer } from \"@noorddev/vlak-react\";\nimport type { AcquisitionStep } from \"@noorddev/vlak-react\";\n\nfunction CapturePlan() {\n  const [steps, setSteps] = useState<readonly AcquisitionStep[]>([\n    { id: \"first\", label: \"First plane\", exposure: 10, exposureUnit: \"ms\", time: 0, timeUnit: \"s\", depth: -1, depthUnit: \"µm\", channel: \"phase\", status: \"Not run\" },\n    { id: \"second\", label: \"Second plane\", exposure: 10, exposureUnit: \"ms\", time: 5, timeUnit: \"s\", depth: 0, depthUnit: \"µm\", channel: \"phase\", status: \"Not run\" },\n  ]);\n  return <AcquisitionSequencer label=\"Capture plan\" channels={[{ id: \"phase\", label: \"Phase\" }]} steps={steps} onStepsChange={setSteps} />;\n}",
      "usage": {
        "use": [
          "Editing and reordering an existing capture plan with onStepsChange.",
          "Pass exact values and units from the acquisition application; the component performs no conversions or scheduling.",
          "Use explicit step actions and onAction to request operations. Supply pending and confirmedLabel from actual host state."
        ],
        "avoid": [
          "Treating an edit or action callback as proof that capture ran successfully.",
          "Deriving instrument settings or executing acquisition. The host owns commands, persistence and confirmations.",
          "More than 128 steps or 128 channels."
        ]
      },
      "keyboard": [
        {
          "keys": "Tab",
          "does": "Moves through enabled numeric inputs, channel selectors and step actions."
        },
        {
          "keys": "Arrow keys, Home, End, typing",
          "does": "Edits native numeric fields or navigates the opened channel list."
        },
        {
          "keys": "Enter, Space",
          "does": "Confirms the active channel option or requests the focused action or reorder."
        },
        {
          "keys": "Escape",
          "does": "Closes the channel list without changing the supplied selection."
        }
      ],
      "a11y": [
        "Step fields and action names include the step number and label, preventing ambiguity between repeated capture names.",
        "Exposure and time offsets use a native non-negative minimum; signed depth values and all supplied units are preserved.",
        "Pending steps disable editing and actions while their last recorded state remains visible.",
        "Controls have 44px targets. Controlled native inputs retain the supplied plan through form resets.",
        "The fieldset ref and native attributes pass through; no callback is interpreted as an actual run confirmation.",
        "Generic controls compose the shared Input, Select and Button primitives; the native numeric fields and Select backing control preserve form values."
      ]
    }
  }
}
