{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "checkbox-tally-notch",
  "title": "Checkbox Tally Notch",
  "description": "Checkbox group rendered as a carpenter's tally: each check carves a notch stroke into its row and adds a stroke to an aggregate tally cluster in the header, where every fifth stroke slashes diagonally across its group of four. Strokes draw on with a dashoffset sweep and retract on uncheck.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/checkbox-tally-notch/component.tsx",
      "content": "\"use client\";\n\nimport { useMemo, useRef, useState } from \"react\";\n\n// Checkbox group rendered as a carpenter's tally: each check carves a notch\n// stroke into the row and adds a stroke to an aggregate tally cluster, where\n// every fifth stroke slashes diagonally across its group of four. Strokes\n// draw on with a dashoffset sweep and retract on uncheck; per-stroke jitter\n// is seeded by index so the carving looks hand-cut but renders identically\n// every mount. Pure DOM+SVG, tokens only, reduced-motion snaps instantly.\n\nexport interface TallyNotchItem {\n  id: string;\n  label: string;\n  hint?: string;\n}\n\nexport interface TallyNotchProps {\n  /** the checkbox rows, in order */\n  items: TallyNotchItem[];\n  /** ids checked at mount */\n  defaultChecked?: string[];\n  /** called with the full list of checked ids after any toggle */\n  onChange?: (checkedIds: string[]) => void;\n  /** heading shown above the group; also the group's accessible name */\n  label?: string;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}\n\n// deterministic per-index jitter — mulberry32, so screenshots are stable\nfunction jitter(i: number) {\n  let t = (i + 1) * 0x6d2b79f5;\n  t = Math.imul(t ^ (t >>> 15), t | 1);\n  t ^= t + Math.imul(t ^ (t >>> 7), t | 61);\n  const r = ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n  return r * 2 - 1; // -1..1\n}\n\n// delay folded into the shorthand itself, not a separate transitionDelay\n// longhand — mixing transition (shorthand) and transitionDelay (longhand)\n// for the same property gives React two owners for one CSS value and it\n// warns (and can genuinely race) on rerender.\nconst STROKE_STYLE = (drawn: boolean, reduced: boolean, delayMs = 0) =>\n  ({\n    strokeDasharray: 1,\n    strokeDashoffset: drawn ? 0 : 1,\n    transition: reduced\n      ? \"none\"\n      : `stroke-dashoffset 260ms ${drawn ? \"cubic-bezier(0.22, 1, 0.36, 1)\" : \"cubic-bezier(0.55, 0, 0.55, 0.2)\"} ${delayMs}ms`,\n  }) as const;\n\nfunction TallyCluster({\n  count,\n  max,\n  reduced,\n}: {\n  count: number;\n  max: number;\n  reduced: boolean;\n}) {\n  const groups = Math.ceil(max / 5);\n  const GW = 30; // group width\n  const H = 26;\n  const paths = useMemo(() => {\n    const out: { d: string; idx: number }[] = [];\n    for (let i = 0; i < max; i++) {\n      const g = Math.floor(i / 5);\n      const k = i % 5;\n      const gx = g * (GW + 10);\n      if (k < 4) {\n        // vertical stroke with seeded lean and length variance\n        const x = gx + 4 + k * 6 + jitter(i) * 1.2;\n        const lean = jitter(i * 7 + 3) * 1.8;\n        const top = 3 + jitter(i * 13 + 5) * 1.4;\n        out.push({ d: `M ${x + lean} ${top} L ${x - lean} ${H - 3}`, idx: i });\n      } else {\n        // the fifth: a diagonal slash across the group of four\n        const y1 = 6 + jitter(i) * 1.5;\n        const y2 = H - 6 + jitter(i * 3) * 1.5;\n        out.push({ d: `M ${gx - 1} ${y1} L ${gx + 23} ${y2}`, idx: i });\n      }\n    }\n    return out;\n  }, [max]);\n\n  return (\n    <svg\n      aria-hidden\n      width={groups * (GW + 10) - 10}\n      height={H}\n      viewBox={`0 0 ${groups * (GW + 10) - 10} ${H}`}\n      className=\"overflow-visible\"\n    >\n      {paths.map((p) => (\n        <path\n          key={p.idx}\n          d={p.d}\n          pathLength={1}\n          fill=\"none\"\n          stroke=\"var(--foreground)\"\n          strokeWidth={p.idx % 5 === 4 ? 2 : 1.75}\n          strokeLinecap=\"round\"\n          // later strokes draw a beat after earlier ones when several\n          // arrive at once (e.g. defaultChecked mount)\n          style={STROKE_STYLE(p.idx < count, reduced, reduced ? 0 : (p.idx % 5) * 30)}\n        />\n      ))}\n    </svg>\n  );\n}\n\nfunction NotchMark({ checked, reduced }: { checked: boolean; reduced: boolean }) {\n  return (\n    <svg aria-hidden width={20} height={20} viewBox=\"0 0 20 20\" className=\"shrink-0\">\n      <rect\n        x={1.5}\n        y={1.5}\n        width={17}\n        height={17}\n        rx={4}\n        fill=\"none\"\n        stroke={checked ? \"var(--foreground)\" : \"var(--border)\"}\n        strokeWidth={1.5}\n        style={{ transition: reduced ? \"none\" : \"stroke 200ms ease\" }}\n      />\n      {/* two-cut carved notch: down-stroke lands, then the long up-stroke */}\n      <path\n        d=\"M 5.5 10.5 L 8.5 13.8\"\n        pathLength={1}\n        fill=\"none\"\n        stroke=\"var(--foreground)\"\n        strokeWidth={2}\n        strokeLinecap=\"round\"\n        style={STROKE_STYLE(checked, reduced)}\n      />\n      <path\n        d=\"M 8.5 13.8 L 14.5 5.8\"\n        pathLength={1}\n        fill=\"none\"\n        stroke=\"var(--foreground)\"\n        strokeWidth={2}\n        strokeLinecap=\"round\"\n        style={STROKE_STYLE(checked, reduced, reduced ? 0 : checked ? 120 : 0)}\n      />\n    </svg>\n  );\n}\n\nexport function TallyNotch({\n  items,\n  defaultChecked = [],\n  onChange,\n  label = \"Tally\",\n  className = \"\",\n}: TallyNotchProps) {\n  const [checked, setChecked] = useState<Set<string>>(() => new Set(defaultChecked));\n  const reduced = useRef(\n    typeof window !== \"undefined\" &&\n      window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches,\n  ).current;\n\n  const toggle = (id: string) => {\n    setChecked((prev) => {\n      const next = new Set(prev);\n      if (next.has(id)) next.delete(id);\n      else next.add(id);\n      onChange?.(items.filter((it) => next.has(it.id)).map((it) => it.id));\n      return next;\n    });\n  };\n\n  return (\n    <div\n      role=\"group\"\n      aria-label={label}\n      className={[\"w-full rounded-md border border-border bg-surface\", className].join(\" \")}\n    >\n      <header className=\"flex items-center justify-between gap-4 border-b border-border px-4 py-3\">\n        <span className=\"font-mono text-xs tracking-widest text-ns-muted uppercase\">{label}</span>\n        <div className=\"flex items-center gap-3\">\n          <TallyCluster count={checked.size} max={items.length} reduced={reduced} />\n          <span\n            aria-live=\"polite\"\n            className=\"min-w-[3ch] text-right font-mono text-xs tabular-nums text-ns-muted\"\n          >\n            {checked.size}/{items.length}\n          </span>\n        </div>\n      </header>\n      <div className=\"flex flex-col\">\n        {items.map((it) => {\n          const isChecked = checked.has(it.id);\n          return (\n            <button\n              key={it.id}\n              type=\"button\"\n              role=\"checkbox\"\n              aria-checked={isChecked}\n              onClick={() => toggle(it.id)}\n              className={[\n                \"group flex cursor-pointer items-center gap-3 border-b border-border px-4 py-3 text-left last:border-b-0\",\n                \"transition-colors duration-150 hover:bg-border/40\",\n                \"focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ns-accent\",\n              ].join(\" \")}\n            >\n              <NotchMark checked={isChecked} reduced={reduced} />\n              <span className=\"flex min-w-0 flex-1 flex-col\">\n                <span\n                  className={[\n                    \"truncate text-sm transition-colors duration-200\",\n                    isChecked ? \"text-ns-muted\" : \"text-foreground\",\n                  ].join(\" \")}\n                >\n                  {it.label}\n                </span>\n                {it.hint ? (\n                  <span className=\"truncate font-mono text-[11px] text-ns-muted\">{it.hint}</span>\n                ) : null}\n              </span>\n            </button>\n          );\n        })}\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/checkbox-tally-notch.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",
      "checklist",
      "form",
      "svg",
      "micro-interaction"
    ],
    "instruction": "Build a checkbox group styled as a carved tally board. STRUCTURE: a bordered bg-surface card (role=group with aria-label from the label prop) whose header carries the group label, an SVG tally cluster, and an aria-live 'checked/total' mono counter; below, one full-width row per item, each a real <button role=checkbox aria-checked> containing a 20px SVG notch mark, the label, and an optional mono hint line. STROKES: every stroke is an SVG <path pathLength={1}> with strokeDasharray 1, drawn by transitioning strokeDashoffset 1 -> 0 over 260ms (ease-out-back-ish cubic-bezier(0.22,1,0.36,1)) and retracted on uncheck with an ease-in curve; the row's notch is two cuts, a short down-stroke then the long up-stroke delayed 120ms so the carve reads as two motions. TALLY CLUSTER: render items.length strokes grouped in fives, four leaning verticals (6px pitch) then a diagonal slash across the group; stroke k is drawn when checkedCount > k, with a 30ms per-position stagger so a batch (e.g. defaultChecked at mount) carves in sequence. Jitter every stroke's lean, length, and endpoints with a deterministic mulberry32-style hash of its index so the carving looks hand-cut but renders byte-identical every mount (stable screenshots). INK: strokes stroke='var(--foreground)', unchecked notch boxes stroke='var(--border)', all chrome from the border/surface/muted tokens — zero hex in markup, both themes render. STATE: uncontrolled Set<string> seeded from defaultChecked, onChange fires with checked ids in items order; checked rows dim their label to text-ns-muted. INTERACTION: rows are native buttons so Space/Enter toggle and Tab reaches every row; hover tints the row bg-border/40; focus-visible draws an inset 2px accent outline. Reduced motion: strokes and notches snap with transition:none. No canvas, no timers, no observers — the only state is the Set, everything else is CSS transitions on SVG attributes."
  },
  "type": "registry:ui"
}