{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "checkbox",
  "title": "Checkbox",
  "description": "No box. The words are the control, and checking strikes them through in the accent.",
  "registryDependencies": [
    "https://thedirectors.agency/ui/r/0nlytype.json",
    "https://thedirectors.agency/ui/r/fraction.json"
  ],
  "files": [
    {
      "path": "registry/0nlytype/ui/checkbox.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { useComposedRefs } from \"@/lib/0nlytype/refs\"\nimport { useFormPresentation } from \"@/lib/0nlytype/use-form-presentation\"\nimport { cn } from \"@/lib/utils\"\nimport { Fraction } from \"@/components/ui/fraction\"\n\nconst GroupSync = React.createContext<(() => void) | null>(null)\n\ntype CheckboxProps = Omit<React.ComponentProps<\"input\">, \"type\"> & {\n  /** The words are the control. */\n  children: React.ReactNode\n  /**\n   * strike: checking strikes the words through, for things done.\n   * stet: checking sets a row of dots under them, the proofreader's \"let it stand\", for things kept.\n   * circled: checking rings them in one pen loop, as you circle a word on a page, for things chosen.\n   */\n  variant?: \"strike\" | \"stet\" | \"circled\"\n  /** Classes for the label, which is the root. className goes to the input. */\n  labelClassName?: string\n  /** Pins a state for documentation (\"hover\", \"focus\"); set on the root. */\n  \"data-force\"?: string\n}\n\n/** No box. The words are the control; checking marks them, by default striking them through in the accent. */\nfunction Checkbox({ children, variant = \"strike\", className, labelClassName, \"data-force\": force, ...props }: CheckboxProps) {\n  const sync = React.useContext(GroupSync)\n  React.useLayoutEffect(() => { sync?.() })\n  return (\n    <label data-slot=\"checkbox\" data-variant={variant} data-force={force} className={cn(\"ot-check\", labelClassName)}>\n      <input type=\"checkbox\" className={className} {...props} />\n      <span>{children}</span>\n      {variant === \"circled\" ? (\n        <svg className=\"ot-check-loop\" viewBox=\"0 0 100 40\" preserveAspectRatio=\"none\" aria-hidden=\"true\">\n          {/* One hand loop: in at the upper left, round clockwise, and on past where it began. */}\n          <path pathLength={1} d=\"M14 9C34 2 78 1 93 11C104 19 96 33 70 37C44 40 12 38 4 28C-3 19 14 8 40 5\" />\n        </svg>\n      ) : null}\n    </label>\n  )\n}\n\ntype CheckboxGroupProps = React.ComponentProps<\"fieldset\"> & {\n  legend?: React.ReactNode\n  /** Show how many are done, as a fraction under the list. */\n  tally?: boolean\n  /** What the tally says: done(count, total). */\n  done?: (count: number, total: number) => React.ReactNode\n}\n\nconst defaultDone = (count: number, total: number) => (count === total ? \"done. Ready when you are.\" : \"done\")\n\n/** A list of checkboxes under a small legend, with an optional tally whose figures turn like a counter's wheels. */\nfunction CheckboxGroup({ legend, tally = false, done = defaultDone, className, children, onChange, ref: forwardedRef, ...props }: CheckboxGroupProps) {\n  const set = React.useRef<HTMLFieldSetElement>(null)\n  const composedRef = useComposedRefs(set, forwardedRef)\n  // null until the first count, so the fraction arrives with its figures and doesn't turn over on load.\n  const [state, setState] = React.useState<{ count: number; total: number } | null>(null)\n\n  const count = React.useCallback(() => {\n    const boxes = [...(set.current?.querySelectorAll<HTMLInputElement>(\"input[type=checkbox]\") ?? [])]\n    const next = { count: boxes.filter((b) => b.checked).length, total: boxes.length }\n    setState((was) => was?.count === next.count && was.total === next.total ? was : next)\n  }, [])\n\n  useFormPresentation(set, count)\n\n  return (\n    <GroupSync.Provider value={count}>\n      <fieldset\n        ref={composedRef}\n        data-slot=\"checkbox-group\"\n        className={cn(\"ot-checklist\", className)}\n        onChange={(e) => {\n          onChange?.(e)\n          if (tally) count()\n        }}\n        {...props}\n      >\n        {legend ? <legend className=\"ot-label\">{legend}</legend> : null}\n        {children}\n      </fieldset>\n      {tally ? (\n        <p data-slot=\"checkbox-tally\" className=\"ot-tally\" aria-live=\"polite\">\n          {state ? (\n            <>\n              <Fraction count={state.count} total={state.total} />\n              <span>{done(state.count, state.total)}</span>\n            </>\n          ) : null}\n        </p>\n      ) : null}\n    </GroupSync.Provider>\n  )\n}\n\nexport { Checkbox, CheckboxGroup, type CheckboxProps, type CheckboxGroupProps }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/0nlytype/styles/checkbox.css",
      "content": "@layer components {\n  /* ── ot-check ── the words only; checking strikes them through */\n  .ot-check { position: relative; display: inline-block; cursor: pointer; color: var(--ot-graphite); -webkit-tap-highlight-color: transparent; }\n  .ot-check > input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; pointer-events: none; }\n  .ot-check > span {\n    --y: 60%;\n    padding-inline: 0.18em; margin-inline: -0.18em; /* the pen runs past the words, as a hand does */\n    background:\n      linear-gradient(var(--ot-accent), var(--ot-accent)) no-repeat 100% var(--y) / 0% var(--ot-stroke),\n      linear-gradient(var(--ot-rule-strong), var(--ot-rule-strong)) no-repeat 100% var(--y) / 0% var(--ot-hairline);\n    -webkit-box-decoration-break: clone; box-decoration-break: clone;\n    transition:\n      background-size var(--ot-moderato) var(--ot-breath),\n      background-position 0s,\n      color var(--ot-moderato) var(--ot-breath),\n      font-weight var(--ot-moderato) var(--ot-breath);\n  }\n  .ot-check:is(:hover, [data-force~=\"hover\"]) > span { color: var(--ot-ink); }\n  /* Pointing sketches the strike in pencil; checking inks it. */\n  .ot-check:is(:hover, [data-force~=\"hover\"]) > input:not(:checked, :disabled) + span { background-position: 100% var(--y), 0 var(--y); background-size: 0% var(--ot-stroke), 100% var(--ot-hairline); }\n  .ot-check > input:checked + span { background-position: 0 var(--y), 100% var(--y); background-size: 100% var(--ot-stroke), 0% var(--ot-hairline); color: var(--ot-pencil); font-weight: 300; }\n  .ot-check:is([dir=\"rtl\"], [dir=\"rtl\"] *) > span { background-position: 0 var(--y), 0 var(--y); }\n  .ot-check:is([dir=\"rtl\"], [dir=\"rtl\"] *):is(:hover, [data-force~=\"hover\"]) > input:not(:checked, :disabled) + span { background-position: 0 var(--y), 100% var(--y); }\n  .ot-check:is([dir=\"rtl\"], [dir=\"rtl\"] *) > input:checked + span { background-position: 100% var(--y), 0 var(--y); }\n  .ot-check > input:focus-visible + span,\n  .ot-check[data-force~=\"focus\"] > span { outline: var(--ot-stroke) solid var(--ot-accent); outline-offset: 4px; }\n  .ot-check[data-force~=\"focus\"] { outline: 0; }\n  .ot-check:has(input:disabled) { cursor: not-allowed; }\n  .ot-check > input:disabled + span { color: var(--ot-pencil); opacity: 0.55; }\n\n  .ot-checklist { border: 0; margin: 0; padding: 0; display: grid; justify-items: start; gap: var(--ot-space-3); font-size: var(--ot-mp); line-height: var(--ot-mp-lh); }\n  .ot-checklist > legend { padding: 0; margin-bottom: var(--ot-space-4); }\n\n  /* The tally: how many are done, as a fraction whose count rolls the way it moves. */\n  .ot-tally { display: flex; align-items: baseline; gap: var(--ot-space-3); margin: var(--ot-space-5) 0 0; font-size: var(--ot-p); color: var(--ot-pencil); }\n  .ot-tally > .ot-fraction > .ot-yours { display: inline-block; }\n\n  /* ── stet and circled ── for things kept or chosen rather than done, where a strike would read as \"no\".\n     Checked, the words are yours, so they turn italic; 1lh keeps the list's line so nothing below moves. */\n  .ot-check:is([data-variant=\"stet\"], [data-variant=\"circled\"]) > span { background: none; }\n  .ot-check:is([data-variant=\"stet\"], [data-variant=\"circled\"]) > input:checked + span {\n    font-family: var(--ot-expression); font-style: italic; font-weight: 400; font-size: calc(var(--ot-expression-scale) * 1em); line-height: 1lh; color: var(--ot-ink);\n  }\n  /* Stet: the proofreader's \"let it stand\", a row of dots under the words. Pointing pencils the dots in; checking\n     sets them down in the accent, a touch lower, with spiccato. */\n  .ot-check[data-variant=\"stet\"] > span {\n    text-decoration: underline dotted transparent; text-decoration-thickness: max(2px, 0.09em); text-underline-offset: 0.2em; text-decoration-skip-ink: none;\n    transition: text-decoration-color var(--ot-allegro) var(--ot-exhale), text-underline-offset var(--ot-moderato) var(--ot-spiccato), color var(--ot-moderato) var(--ot-breath);\n  }\n  .ot-check[data-variant=\"stet\"]:is(:hover, [data-force~=\"hover\"]) > input:not(:checked, :disabled) + span { text-decoration-color: var(--ot-rule-strong); }\n  .ot-check[data-variant=\"stet\"] > input:checked + span { text-decoration-color: var(--ot-accent); text-underline-offset: 0.3em; }\n  /* Circled: one pen loop round the words, the accent rings of the collage. It comes in at the upper left and runs on\n     past where it began, as a hand never quite closes a circle. Pointing draws it in pencil; checking inks it. A loop\n     has no heading to leave by, so it fades, then resets unseen. */\n  .ot-check-loop {\n    position: absolute; left: calc(-7% - 0.4em); top: -0.44em; width: calc(114% + 0.8em); height: calc(100% + 0.84em); max-width: none; /* an ellipse must stand wider than the words to clear their corners */ overflow: visible; pointer-events: none;\n    fill: none; stroke: var(--ot-rule-strong); stroke-width: var(--ot-hairline); stroke-linecap: round; stroke-dasharray: 1 1; stroke-dashoffset: 1; opacity: 0;\n    transition: opacity var(--ot-allegro) var(--ot-breath), stroke-dashoffset 0s var(--ot-allegro), stroke var(--ot-allegro), stroke-width var(--ot-allegro);\n  }\n  .ot-check-loop > path { vector-effect: non-scaling-stroke; }\n  .ot-check:is([dir=\"rtl\"], [dir=\"rtl\"] *) > .ot-check-loop { scale: -1 1; }\n  .ot-check:is(:hover, [data-force~=\"hover\"]) > input:not(:checked, :disabled) ~ .ot-check-loop,\n  .ot-check > input:checked ~ .ot-check-loop { opacity: 1; stroke-dashoffset: 0; transition: opacity 0s, stroke-dashoffset var(--ot-andante) var(--ot-exhale), stroke var(--ot-allegro), stroke-width var(--ot-allegro); }\n  .ot-check > input:checked ~ .ot-check-loop { stroke: var(--ot-accent); stroke-width: var(--ot-stroke); }\n  .ot-check > input:disabled ~ .ot-check-loop { opacity: 0.35; }\n  /* Forced colours drop background images, which draw the strike, and force the stet dots' transparent colour to\n     text: restate both as plain text decoration, so checked and unchecked still differ. */\n  @media (forced-colors: active) {\n    .ot-check:not([data-variant=\"stet\"], [data-variant=\"circled\"]) > input:checked + span { text-decoration: line-through; }\n    .ot-check[data-variant=\"stet\"] > span { text-decoration-line: none; }\n    .ot-check[data-variant=\"stet\"] > input:checked + span { text-decoration-line: underline; }\n    .ot-check-loop { stroke: CanvasText; }\n    .ot-check > input:checked ~ .ot-check-loop { stroke: Highlight; }\n  }\n}\n",
      "type": "registry:file",
      "target": "styles/0nlytype/checkbox.css"
    }
  ],
  "css": {
    "@import \"../styles/0nlytype/checkbox.css\"": {}
  },
  "meta": {
    "movement": "VI",
    "underneath": "native",
    "contract": "ot-check"
  },
  "type": "registry:ui"
}
