{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "layer-stack",
  "type": "registry:component",
  "title": "Layer stack",
  "description": "Manages supplied layer selection, visibility, locking, and order through named 44px controls and caller-owned changes.",
  "registryDependencies": [
    "https://vlak.dev/r/vlak-base.json",
    "https://vlak.dev/r/inter.json",
    "https://vlak.dev/r/button.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/layer-stack.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 { Button } from \"./button\";\n\nexport interface CreativeLayer { id: string; label: string; visible: boolean; locked: boolean }\nexport interface LayerStackProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onSelect\"> {\n  label: React.ReactNode;\n  /** Ordered from top to bottom. */\n  layers: CreativeLayer[];\n  selectedId?: string | null;\n  onSelect?: (id: string) => void;\n  onLayersChange?: (layers: CreativeLayer[]) => void;\n  disabled?: boolean;\n}\n\nconst styles = stylex.create({\n  root: { display: \"grid\", gap: \"0.75rem\", minWidth: 0, color: vlak.ink },\n  label: { margin: 0, fontSize: vlak.controlFs, fontWeight: 600, lineHeight: 1.45 },\n  list: { display: \"grid\", gap: \"0.5rem\", listStyleType: \"none\", margin: 0, padding: 0 },\n  row: { display: \"grid\", gap: \"0.5rem\", borderWidth: vlak.hairline, borderStyle: \"solid\", borderColor: vlak.divider, padding: \"0.5rem\" },\n  actions: { display: \"flex\", flexWrap: \"wrap\", gap: \"0.375rem\" },\n  action: { minWidth: vlak.hit, minHeight: vlak.hit, width: \"auto\", maxWidth: \"100%\" },\n  button: { minWidth: vlak.hit, minHeight: vlak.hit, paddingInline: \"0.625rem\", borderWidth: vlak.hairline, borderStyle: \"solid\", borderColor: vlak.controlBorder, borderRadius: vlak.radiusSm, backgroundColor: vlak.paper, color: vlak.ink, fontFamily: \"inherit\", fontSize: vlak.controlLabel, lineHeight: 1.45, cursor: \"pointer\", outlineWidth: { default: null, \":focus-visible\": 2 }, outlineStyle: { default: null, \":focus-visible\": \"solid\" }, outlineColor: vlak.ink, outlineOffset: 2 },\n  selected: { backgroundColor: { default: vlak.ink, [mq.forcedColors]: \"Highlight\" }, color: { default: vlak.paper, [mq.forcedColors]: \"HighlightText\" } },\n  note: { margin: 0, fontSize: vlak.controlLabel, color: vlak.gray, lineHeight: 1.45 },\n});\n\n/** A controlled layer order with explicit selection, visibility, lock and move actions. */\nexport const LayerStack = React.forwardRef<HTMLDivElement, LayerStackProps>(function LayerStack({ label, layers, selectedId, onSelect, onLayersChange, disabled, className, style, ...props }, ref) {\n  const root = rs([\"rs-layer-stack\", className], styles.root);\n  const heading = rs([\"rs-layer-stack-label\"], styles.label);\n  const list = rs([\"rs-layer-stack-list\"], styles.list);\n  const row = rs([\"rs-layer-stack-row\"], styles.row);\n  const actions = rs([\"rs-layer-stack-actions\"], styles.actions);\n  const note = rs([\"rs-layer-stack-note\"], styles.note);\n  const action = rs([\"rs-layer-stack-action\"], styles.action);\n  const update = (id: string, patch: Partial<CreativeLayer>) => onLayersChange?.(layers.map((layer) => layer.id === id ? { ...layer, ...patch } : layer));\n  const move = (index: number, delta: number) => {\n    const next = [...layers];\n    const target = index + delta;\n    if (target < 0 || target >= next.length) return;\n    [next[index], next[target]] = [next[target]!, next[index]!];\n    onLayersChange?.(next);\n  };\n  return <div {...props} ref={ref} className={root.className} style={{ ...root.style, ...style }}>\n    <p className={heading.className} style={heading.style}>{label}</p>\n    {layers.length === 0 && <p className={note.className} style={note.style}>No layers</p>}\n    <ol className={list.className} style={list.style}>{layers.map((layer, index) => {\n      const selected = layer.id === selectedId;\n      const select = rs([\"rs-layer-stack-button\", selected && \"rs-layer-stack-selected\"], styles.button, selected && styles.selected);\n      return <li key={layer.id} className={row.className} style={row.style}>\n        {onSelect ? <button type=\"button\" disabled={disabled} aria-pressed={selected} className={select.className} style={select.style} onClick={() => onSelect(layer.id)}>{layer.label}</button> : <span>{layer.label}{selected ? \" · Selected\" : \"\"}</span>}\n        <span className={note.className} style={note.style}>{layer.visible ? \"Visible\" : \"Hidden\"} · {layer.locked ? \"Locked\" : \"Unlocked\"}</span>\n        {onLayersChange && <div className={actions.className} style={actions.style}>\n          <Button variant=\"ghost\" size=\"sm\" disabled={disabled} aria-label={`${layer.label} visibility`} aria-pressed={layer.visible} {...action} onClick={() => update(layer.id, { visible: !layer.visible })}>Visibility</Button>\n          <Button variant=\"ghost\" size=\"sm\" disabled={disabled} aria-label={`${layer.label} lock`} aria-pressed={layer.locked} {...action} onClick={() => update(layer.id, { locked: !layer.locked })}>Lock</Button>\n          <Button variant=\"ghost\" size=\"sm\" disabled={disabled || layer.locked || index === 0} aria-label={`Move ${layer.label} up`} {...action} onClick={() => move(index, -1)}>↑</Button>\n          <Button variant=\"ghost\" size=\"sm\" disabled={disabled || layer.locked || index === layers.length - 1} aria-label={`Move ${layer.label} down`} {...action} onClick={() => move(index, 1)}>↓</Button>\n        </div>}\n      </li>;\n    })}</ol>\n  </div>;\n});\n",
      "type": "registry:component",
      "target": "components/vlak/layer-stack.tsx"
    },
    {
      "path": "vlak/styles/layer-stack.css",
      "content": "/* ── layer-stack: generated from packages/react/src/components/layer-stack.tsx ── */\n.rs-layer-stack{display:grid;gap:0.75rem;min-width:0;color:var(--text)}\n.rs-layer-stack-label{margin:0;font-size:var(--control-fs);font-weight:600;line-height:1.45}\n.rs-layer-stack-list{display:grid;gap:0.5rem;list-style-type:none;margin:0;padding:0}\n.rs-layer-stack-row{display:grid;gap:0.5rem;border-width:1px;border-style:solid;border-color:var(--divider);padding:0.5rem}\n.rs-layer-stack-actions{display:flex;flex-wrap:wrap;gap:0.375rem}\n.rs-layer-stack-action{min-width:var(--hit);min-height:var(--hit);width:auto;max-width:100%}\n.rs-layer-stack-button{min-width:var(--hit);min-height:var(--hit);padding-inline:0.625rem;border-width:1px;border-style:solid;border-color:var(--control-border);border-radius:var(--radius-sm);background-color:var(--bg);color:var(--text);font-family:inherit;font-size:var(--control-label);line-height:1.45;cursor:pointer;outline-color:var(--text);outline-offset:2px}\n.rs-layer-stack-button:focus-visible{outline-width:2px;outline-style:solid}\n.rs-layer-stack-selected{background-color:var(--text);color:var(--bg)}\n@media (forced-colors: active){.rs-layer-stack-selected{background-color:Highlight;color:HighlightText}}\n.rs-layer-stack-note{margin:0;font-size:var(--control-label);color:var(--text-secondary);line-height:1.45}\n",
      "type": "registry:file",
      "target": "styles/vlak/layer-stack.css"
    }
  ],
  "meta": {
    "vlak": {
      "category": "creative",
      "classes": [
        "rs-layer-stack",
        "rs-layer-stack-label",
        "rs-layer-stack-list",
        "rs-layer-stack-row",
        "rs-layer-stack-actions",
        "rs-layer-stack-action",
        "rs-layer-stack-button",
        "rs-layer-stack-selected",
        "rs-layer-stack-note"
      ],
      "snippet": "<div class=\"rs-layer-stack\"><p class=\"rs-layer-stack-label\">Layers</p><ol class=\"rs-layer-stack-list\"><li class=\"rs-layer-stack-row\"><span>Title</span><span class=\"rs-layer-stack-note\">Visible · Unlocked</span></li><li class=\"rs-layer-stack-row\"><span>Background</span><span class=\"rs-layer-stack-note\">Visible · Locked</span></li></ol></div>",
      "cssOnly": false,
      "registryDependencies": [
        "button",
        "vlak-lib"
      ],
      "aliases": [
        "LayerStack",
        "Layers panel",
        "Layer list",
        "Layer inspector",
        "Object stack"
      ],
      "example": "import { useState } from \"react\";\nimport { LayerStack } from \"@noorddev/vlak-react\";\nimport type { CreativeLayer } from \"@noorddev/vlak-react\";\n\nfunction Layers() {\n  const [layers, setLayers] = useState<CreativeLayer[]>([{ id: \"title\", label: \"Title\", visible: true, locked: false }, { id: \"background\", label: \"Background\", visible: true, locked: true }]);\n  const [selected, setSelected] = useState<string | null>(null);\n  return <LayerStack label=\"Layers\" layers={layers} selectedId={selected} onSelect={setSelected} onLayersChange={setLayers} />;\n}",
      "usage": {
        "use": [
          "Top-to-bottom layer order for a graphics, audio, or video editor.",
          "onLayersChange receives the proposed array; the application applies it and owns persistence.",
          "onSelect independently controls selection; locked layers cannot use their move actions."
        ],
        "avoid": [
          "Implying drag-and-drop support or modifying document content behind the host's back.",
          "Rendering state-changing controls without onLayersChange."
        ]
      },
      "keyboard": [
        {
          "keys": "Tab",
          "does": "Moves through available layer selection and Vlak visibility, lock, and move buttons."
        },
        {
          "keys": "Enter, Space",
          "does": "Requests the focused action; move buttons reorder one position when allowed."
        }
      ],
      "a11y": [
        "An ordered list preserves the supplied layer order, with visible status text for visibility and locking.",
        "Selection, visibility, and lock controls have stable names and pressed states.",
        "Move actions name their layer, disable at boundaries or when locked, and avoid a drag-only workflow.",
        "All actions have 44px targets; the ref and native attributes reach the root div."
      ]
    }
  }
}
