{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "checkbox-domino-run",
  "title": "Checkbox Domino Run",
  "description": "Select-all where the change propagates like a domino run: flip the master and a wavefront tips down the list, each row's checkbox flipping and shoving the next — click any row mid-run to halt the wave there, leaving the rest untouched.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/checkbox-domino-run/component.tsx",
      "content": "\"use client\";\n\nimport {\n  useCallback,\n  useEffect,\n  useId,\n  useRef,\n  useState,\n  type ChangeEvent,\n} from \"react\";\n\n// ---------------------------------------------------------------------------\n// ToppleRun — a select-all whose propagation is the interaction model, not a\n// cosmetic stagger. Flipping the master checkbox releases a wavefront that\n// travels down the list at a fixed rate (14 rows/sec): as the front reaches\n// row i, that row's own checkbox commits its new state under a 120ms\n// spring-eased transition and the row takes a transient translateY(2px)\n// lean toward row i+1 — read as the wave shoving the next domino — handed\n// off with a short overlap so consecutive leans read as continuous contact.\n// A thin tick travels the list's left edge marking the front's live\n// position. Every row commits its state the instant the front passes it, so\n// there is nothing to roll back: stopping the run just stops future rows\n// from being touched.\n//\n// INTERRUPTION: the run is spatially interruptible, not just cancelable —\n// activating ANY row (pointer or keyboard, since every row stays a fully\n// operable checkbox for the run's entire duration) halts the wave\n// immediately. Rows the front already passed keep their committed state;\n// rows beyond the front are simply untouched, standing exactly as they\n// were. Escape halts it too. This is the \"all except a few\" recovery path:\n// watch the run, stop it where you want, no undo needed because nothing\n// past that point was ever touched.\n//\n// A11Y: the master is a real input[type=checkbox] with aria-controls\n// pointing at the row list's id; a polite live region announces once at\n// the start (\"Enabling all…\") and once at the end or halt (\"Enabled 34 of\n// 40, stopped at row 35.\"). Every row is a real checkbox with its own\n// native label association — nothing here is conveyed only by animation.\n//\n// REDUCED MOTION: there is no wave to catch, so interruption is replaced by\n// a straightforward safety net — the master flips every row in a single\n// frame and a 5s Undo affordance appears, reverting the whole batch if\n// pressed in time.\n//\n// TOKENS: --foreground for ink (checkbox fill, the traveling tick),\n// --border for hairlines, --ns-muted for secondary text, --ns-accent for the\n// focus ring only. Pure DOM/CSS, no canvas.\n// ---------------------------------------------------------------------------\n\nconst ROWS_PER_SEC = 14;\nconst ROW_H = 56; // px — must match the row wrapper's fixed height\nconst LEAN_MS = Math.round(1000 / ROWS_PER_SEC) + 40; // ~111ms, overlaps the next row's start\nconst UNDO_MS = 5000;\n\nexport interface ToppleRunItem {\n  /** Stable identifier for this row. */\n  id: string;\n  /** Primary row label — also becomes its checkbox's accessible name. */\n  label: string;\n  /** Optional secondary line (sender, timestamp, byline...). */\n  description?: string;\n}\n\nexport interface ToppleRunProps {\n  /** Rows the master toggle governs, top to bottom = wave direction. */\n  items: ToppleRunItem[];\n  /** Label for the master control. @default \"Select all\" */\n  label?: string;\n  /** Ids checked on mount. @default [] */\n  defaultChecked?: string[];\n  /** Fires whenever the checked set changes, including mid-run commits. */\n  onChange?: (checkedIds: string[]) => void;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}\n\nfunction useReducedMotion() {\n  const [reduced, setReduced] = useState(false);\n  useEffect(() => {\n    const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    const onChange = () => setReduced(mq.matches);\n    onChange();\n    mq.addEventListener(\"change\", onChange);\n    return () => mq.removeEventListener(\"change\", onChange);\n  }, []);\n  return reduced;\n}\n\ninterface Row extends ToppleRunItem {\n  checked: boolean;\n}\n\nexport function ToppleRun({\n  items,\n  label = \"Select all\",\n  defaultChecked = [],\n  onChange,\n  className = \"\",\n}: ToppleRunProps) {\n  const uid = useId();\n  const reducedMotion = useReducedMotion();\n\n  const [rows, setRows] = useState<Row[]>(() =>\n    items.map((item) => ({\n      ...item,\n      checked: defaultChecked.includes(item.id),\n    }))\n  );\n  const [running, setRunning] = useState(false);\n  const [liveMsg, setLiveMsg] = useState(\"\");\n  const [undoAvailable, setUndoAvailable] = useState(false);\n\n  const masterRef = useRef<HTMLInputElement | null>(null);\n  const rowElRefs = useRef<(HTMLLIElement | null)[]>([]);\n  const tickRef = useRef<HTMLDivElement | null>(null);\n\n  const rafRef = useRef<number | undefined>(undefined);\n  const runTokenRef = useRef(0);\n  const committedRef = useRef(-1);\n  const startTimeRef = useRef(0);\n  const targetRef = useRef(true);\n  const leanTimersRef = useRef<Map<number, ReturnType<typeof setTimeout>>>(\n    new Map()\n  );\n  const undoSnapshotRef = useRef<Row[] | null>(null);\n  const undoTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(\n    undefined\n  );\n\n  const total = rows.length;\n  const checkedCount = rows.reduce((n, r) => (r.checked ? n + 1 : n), 0);\n  const allChecked = total > 0 && checkedCount === total;\n  const noneChecked = checkedCount === 0;\n  const indeterminate = !allChecked && !noneChecked;\n\n  useEffect(() => {\n    if (masterRef.current) masterRef.current.indeterminate = indeterminate;\n  }, [indeterminate]);\n\n  const firstRender = useRef(true);\n  useEffect(() => {\n    if (firstRender.current) {\n      firstRender.current = false;\n      return;\n    }\n    onChange?.(rows.filter((r) => r.checked).map((r) => r.id));\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [rows]);\n\n  const clearLeanTimers = useCallback(() => {\n    leanTimersRef.current.forEach((t) => clearTimeout(t));\n    leanTimersRef.current.clear();\n  }, []);\n\n  const commitRow = useCallback((index: number, checked: boolean) => {\n    setRows((prev) => {\n      if (prev[index]?.checked === checked) return prev;\n      const next = prev.slice();\n      next[index] = { ...next[index]!, checked };\n      return next;\n    });\n    const el = rowElRefs.current[index];\n    if (el) {\n      el.style.transform = \"translateY(2px)\";\n      const prevTimer = leanTimersRef.current.get(index);\n      if (prevTimer) clearTimeout(prevTimer);\n      leanTimersRef.current.set(\n        index,\n        setTimeout(() => {\n          el.style.transform = \"\";\n          leanTimersRef.current.delete(index);\n        }, LEAN_MS)\n      );\n    }\n  }, []);\n\n  const endRun = useCallback(\n    (committedCount: number, rowTotal: number) => {\n      if (rafRef.current !== undefined) {\n        cancelAnimationFrame(rafRef.current);\n        rafRef.current = undefined;\n      }\n      setRunning(false);\n      if (tickRef.current) tickRef.current.style.opacity = \"0\";\n      const verb = targetRef.current ? \"Enabled\" : \"Disabled\";\n      if (rowTotal === 0) {\n        setLiveMsg(\"\");\n      } else if (committedCount >= rowTotal) {\n        setLiveMsg(`${verb} all ${rowTotal}.`);\n      } else {\n        setLiveMsg(\n          `${verb} ${committedCount} of ${rowTotal}, stopped at row ${\n            committedCount + 1\n          }.`\n        );\n      }\n    },\n    []\n  );\n\n  const haltRun = useCallback(() => {\n    if (!running) return;\n    runTokenRef.current += 1;\n    endRun(committedRef.current + 1, total);\n  }, [running, endRun, total]);\n\n  const startRun = useCallback(\n    (target: boolean) => {\n      if (total === 0) return;\n      if (rafRef.current !== undefined) cancelAnimationFrame(rafRef.current);\n      clearLeanTimers();\n\n      runTokenRef.current += 1;\n      const token = runTokenRef.current;\n      targetRef.current = target;\n      committedRef.current = -1;\n      startTimeRef.current = performance.now();\n      setRunning(true);\n      setLiveMsg(target ? \"Enabling all…\" : \"Disabling all…\");\n      if (tickRef.current) {\n        tickRef.current.style.opacity = \"1\";\n        tickRef.current.style.transform = \"translateY(0px)\";\n      }\n\n      const step = (now: number) => {\n        if (runTokenRef.current !== token) return;\n        const elapsedS = (now - startTimeRef.current) / 1000;\n        const front = elapsedS * ROWS_PER_SEC;\n        if (tickRef.current) {\n          tickRef.current.style.transform = `translateY(${\n            Math.min(front, total) * ROW_H\n          }px)`;\n        }\n        while (committedRef.current + 1 < total && committedRef.current + 1 <= front) {\n          const i = committedRef.current + 1;\n          commitRow(i, target);\n          committedRef.current = i;\n        }\n        if (committedRef.current >= total - 1) {\n          endRun(total, total);\n          return;\n        }\n        rafRef.current = requestAnimationFrame(step);\n      };\n      rafRef.current = requestAnimationFrame(step);\n    },\n    [total, clearLeanTimers, commitRow, endRun]\n  );\n\n  // Escape halts a live run from anywhere.\n  useEffect(() => {\n    if (!running) return;\n    const onKey = (e: KeyboardEvent) => {\n      if (e.key === \"Escape\") haltRun();\n    };\n    window.addEventListener(\"keydown\", onKey);\n    return () => window.removeEventListener(\"keydown\", onKey);\n  }, [running, haltRun]);\n\n  useEffect(() => {\n    return () => {\n      if (rafRef.current !== undefined) cancelAnimationFrame(rafRef.current);\n      clearLeanTimers();\n      if (undoTimerRef.current) clearTimeout(undoTimerRef.current);\n    };\n  }, [clearLeanTimers]);\n\n  const handleMasterChange = useCallback(\n    (e: ChangeEvent<HTMLInputElement>) => {\n      const target = e.target.checked;\n      if (total === 0) return;\n\n      if (reducedMotion) {\n        if (undoTimerRef.current) clearTimeout(undoTimerRef.current);\n        undoSnapshotRef.current = rows.map((r) => ({ ...r }));\n        setRows((prev) => prev.map((r) => ({ ...r, checked: target })));\n        setLiveMsg(\n          target ? `Enabled all ${total}.` : `Disabled all ${total}.`\n        );\n        setUndoAvailable(true);\n        undoTimerRef.current = setTimeout(() => {\n          setUndoAvailable(false);\n          undoSnapshotRef.current = null;\n        }, UNDO_MS);\n        return;\n      }\n\n      startRun(target);\n    },\n    [total, reducedMotion, rows, startRun]\n  );\n\n  const handleRowChange = useCallback(\n    (index: number, checked: boolean) => {\n      if (running) haltRun();\n      setRows((prev) => {\n        const next = prev.slice();\n        next[index] = { ...next[index]!, checked };\n        return next;\n      });\n    },\n    [running, haltRun]\n  );\n\n  const handleUndo = useCallback(() => {\n    if (undoTimerRef.current) clearTimeout(undoTimerRef.current);\n    if (undoSnapshotRef.current) {\n      const snapshot = undoSnapshotRef.current;\n      setRows(snapshot);\n      setLiveMsg(\"Undone.\");\n    }\n    setUndoAvailable(false);\n    undoSnapshotRef.current = null;\n  }, []);\n\n  const groupId = `checkbox-domino-run-${uid}-group`;\n  const liveId = `checkbox-domino-run-${uid}-live`;\n\n  return (\n    <div className={`w-full rounded-md border border-border bg-surface ${className}`}>\n      <div id={liveId} aria-live=\"polite\" className=\"sr-only\">\n        {liveMsg}\n      </div>\n\n      <label className=\"relative flex h-14 cursor-pointer select-none items-center gap-3 border-b border-border px-4\">\n        <input\n          ref={masterRef}\n          type=\"checkbox\"\n          className=\"peer absolute inset-0 z-10 h-full w-full cursor-pointer opacity-0 focus:outline-none\"\n          checked={allChecked}\n          onChange={handleMasterChange}\n          aria-controls={groupId}\n        />\n        <CheckboxVisual\n          checked={allChecked}\n          indeterminate={indeterminate}\n          reducedMotion={reducedMotion}\n        />\n        <span className=\"flex-1 text-[13px] font-medium text-foreground\">\n          {label}\n        </span>\n        <span aria-hidden=\"true\" className=\"font-mono text-[11px] text-ns-muted\">\n          {checkedCount}/{total}\n        </span>\n      </label>\n\n      <div className=\"relative\">\n        <div\n          ref={tickRef}\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute left-0 top-0 z-10 w-[2px] bg-foreground opacity-0\"\n          style={{ height: ROW_H, willChange: \"transform\" }}\n        />\n        <ul id={groupId}>\n          {rows.map((row, i) => (\n            <li\n              key={row.id}\n              ref={(el) => {\n                rowElRefs.current[i] = el;\n              }}\n              style={{\n                height: ROW_H,\n                transition: reducedMotion\n                  ? undefined\n                  : `transform ${LEAN_MS}ms cubic-bezier(0.34, 1.56, 0.64, 1)`,\n              }}\n              className={i < rows.length - 1 ? \"border-b border-border\" : \"\"}\n            >\n              <label className=\"relative flex h-full cursor-pointer select-none items-center gap-3 px-4\">\n                <input\n                  type=\"checkbox\"\n                  className=\"peer absolute inset-0 z-10 h-full w-full cursor-pointer opacity-0 focus:outline-none\"\n                  checked={row.checked}\n                  onChange={(e) => handleRowChange(i, e.target.checked)}\n                />\n                <CheckboxVisual\n                  checked={row.checked}\n                  reducedMotion={reducedMotion}\n                />\n                <span className=\"min-w-0 flex-1\">\n                  <span className=\"block truncate text-[13px] text-foreground\">\n                    {row.label}\n                  </span>\n                  {row.description && (\n                    <span className=\"block truncate text-[12px] text-ns-muted\">\n                      {row.description}\n                    </span>\n                  )}\n                </span>\n              </label>\n            </li>\n          ))}\n        </ul>\n      </div>\n\n      {undoAvailable && (\n        <div className=\"flex items-center justify-between gap-3 border-t border-border px-4 py-3\">\n          <span className=\"text-[12px] text-ns-muted\">\n            Batch applied — undo within {UNDO_MS / 1000}s.\n          </span>\n          <button\n            type=\"button\"\n            onClick={handleUndo}\n            className=\"rounded-sm border border-border px-2.5 py-1 text-[12px] font-medium text-foreground transition-colors duration-150 hover:border-foreground/40 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ns-accent\"\n          >\n            Undo\n          </button>\n        </div>\n      )}\n    </div>\n  );\n}\n\nfunction CheckboxVisual({\n  checked,\n  indeterminate = false,\n  reducedMotion = false,\n}: {\n  checked: boolean;\n  indeterminate?: boolean;\n  reducedMotion?: boolean;\n}) {\n  const springDuration = reducedMotion ? \"0ms\" : \"120ms\";\n  const springEase = \"cubic-bezier(0.34, 1.56, 0.64, 1)\";\n  return (\n    <span\n      aria-hidden=\"true\"\n      className=\"relative inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-[6px] border border-border bg-background peer-checked:border-foreground peer-checked:bg-foreground peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-ns-accent\"\n      style={{\n        transition: `background-color ${springDuration} ${springEase}, border-color ${springDuration} ${springEase}`,\n      }}\n    >\n      <svg\n        viewBox=\"0 0 16 16\"\n        className=\"h-3 w-3 text-background\"\n        style={{\n          opacity: checked && !indeterminate ? 1 : 0,\n          transform: checked && !indeterminate ? \"scale(1)\" : \"scale(0.5)\",\n          transition: `opacity ${springDuration} ${springEase}, transform ${springDuration} ${springEase}`,\n        }}\n      >\n        <path\n          d=\"M3 8.2L6.2 11.4L13 4.4\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth={2}\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        />\n      </svg>\n      <span\n        className=\"absolute h-[2px] w-2.5 rounded-full bg-background\"\n        style={{\n          opacity: indeterminate ? 1 : 0,\n          transition: `opacity ${springDuration} ${springEase}`,\n        }}\n      />\n    </span>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/checkbox-domino-run.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ns-muted": "var(--ns-muted)",
      "color-ns-accent": "var(--ns-accent)",
      "color-surface": "var(--surface)"
    },
    "light": {
      "ns-muted": "#4d4d4d",
      "ns-accent": "#006bff",
      "surface": "#fafafa"
    },
    "dark": {
      "ns-muted": "#8f8f8f",
      "surface": "#171717"
    }
  },
  "meta": {
    "collection": "core",
    "tags": [
      "checkbox",
      "select-all",
      "bulk-action",
      "list",
      "form",
      "micro-interaction"
    ],
    "instruction": "A select-all/toggle-all whose change propagates like a domino run rather than a cosmetic stagger: flipping the master checkbox (real input[type=checkbox], aria-controls pointing at the row list's id) releases an rAF-driven wavefront index that advances at a fixed 14 rows/sec. As the front reaches row i, that row's own checkbox commits its new checked state (a 120ms CSS transition on fill/border with a slight spring overshoot, cubic-bezier(0.34,1.56,0.64,1)) and the row's <li> takes a transient translateY(2px) lean toward row i+1, held for roughly one row-interval plus a 40ms overlap so consecutive leans read as continuous handed-off contact rather than independent bumps. A thin 2px --foreground tick travels down the list's left edge on the same rAF loop, marking the front's live position. State commits per row the instant the front passes it — nothing is staged or rolled back, so stopping the run costs nothing. INTERRUPTION IS FIRST-CLASS: every row stays a fully operable native checkbox for the run's entire duration; activating ANY row, by pointer or keyboard, or pressing Escape, halts the wave immediately. Rows the front already passed keep their committed state; rows beyond the front are simply never touched, standing exactly as they were — this is the 'all except a few' path, no undo needed because nothing past that point was ever touched. A11Y: a polite live region announces once at the start ('Enabling all…'/'Disabling all…') and once at the end or halt ('Enabled 34 of 40, stopped at row 35.'), throttled to those two moments only — not per row. Every row and the master are real input[type=checkbox] elements with native label association for their accessible name; the master's own live counter is aria-hidden decoration, not part of its name. REDUCED MOTION: there is no wave to catch, so interruption is replaced by a plain safety net — the master flips every row's state in a single frame (no wavefront, no lean, no tick) and a 5s Undo affordance appears beneath the list, reverting the whole batch if pressed in time. TOKENS: --foreground for the checkbox fill and the traveling tick, --border for hairlines between rows, --ns-muted for secondary row text and the counter, --ns-accent for the focus ring only. Pure DOM/CSS — no canvas, no SVG filters."
  },
  "type": "registry:ui"
}