{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "checkbox",
  "type": "registry:ui",
  "title": "Checkbox",
  "description": "An accessible React checkbox for Next.js forms, with a check that draws itself in and an indeterminate state, built on Radix Checkbox.",
  "author": "FewTone <https://fewtone.com>",
  "dependencies": [
    "class-variance-authority@^0.7.1",
    "motion@^14.0.0",
    "radix-ui@^1.7.0"
  ],
  "registryDependencies": [
    "https://ui.fewtone.com/r/tokens.json",
    "https://ui.fewtone.com/r/motion.json"
  ],
  "files": [
    {
      "path": "registry/fewtone/ui/checkbox.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { duration, ease } from \"@/lib/motion\";\nimport { cn } from \"@/lib/utils\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { Checkbox as CheckboxPrimitive } from \"radix-ui\";\nimport { type ComponentProps, useState } from \"react\";\n\n/*\n * A checkbox on Radix Checkbox (owner, 2026-10-15).\n * - Motion (.agents/rules/motion.md): the fill grows from the centre as it fades in (CSS, the same `before:`\n *   layer as Button's tint) and the check draws itself in (pathLength, the tiny-stroke exception).\n *   Unchecking is checking played backwards: the check erases, then the fill shrinks and fades. Indeterminate is a dash that draws in the same way;\n *   between checked and indeterminate the fill stays and one mark erases as the other draws.\n * - Nothing animates on first paint. With reduced motion the marks appear and go at once.\n * - The state is held here and always passed to Radix, controlled or not, so the marks know what to draw.\n *   When its form resets, Radix restores the first state through onCheckedChange, so that still works.\n * - Sizes match Switch's track heights (16 and 20px), so the two line up in a form. Flat, no shadow.\n */\n\nconst checkboxVariants = cva(\n  [\n    \"peer relative isolate inline-flex shrink-0 cursor-pointer items-center justify-center border border-input bg-transparent text-primary-foreground\",\n    \"transition-colors duration-(--duration-fast) ease-out enabled:hover:border-ring\",\n    // The fill covers the border, so checked is one solid shape. It grows from 60% as it fades in (owner,\n    // 2026-10-15: \"the BG can also animate a little\"). Unchecking, it waits for the check to start erasing, then\n    // shrinks and fades a little quicker.\n    \"before:pointer-events-none before:absolute before:-inset-px before:-z-10 before:bg-primary before:opacity-0 before:scale-60\",\n    \"before:transition before:duration-(--duration-fast) before:ease-in data-[state=unchecked]:before:delay-(--duration-instant)\",\n    \"not-data-[state=unchecked]:before:scale-100 not-data-[state=unchecked]:before:opacity-100 not-data-[state=unchecked]:before:duration-(--duration-base) not-data-[state=unchecked]:before:ease-out\",\n    \"outline-ring focus-visible:outline-(length:--focus-ring-width) focus-visible:outline-offset-(--focus-ring-offset)\",\n    \"aria-invalid:border-destructive aria-invalid:outline-destructive\",\n    \"disabled:cursor-not-allowed disabled:opacity-(--opacity-disabled)\",\n  ],\n  {\n    variants: {\n      size: {\n        sm: \"size-4 rounded-xs before:rounded-xs\",\n        md: \"size-5 rounded-sm before:rounded-sm\",\n      },\n    },\n    defaultVariants: { size: \"md\" },\n  },\n);\n\n/** The same check as Copy Button and Loading Button, and a dash for indeterminate */\nconst CHECK = \"M5 12.5l4.5 4.5L19 7.5\";\nconst DASH = \"M7 12h10\";\n\n/**\n * A mark that draws itself in, after the fill has started, and erases the way it came. Its opacity goes\n * with it, so a round cap never leaves a dot behind.\n */\nfunction Mark({ d, shown, reduce }: { d: string; shown: boolean; reduce: boolean }) {\n  return (\n    <motion.path\n      d={d}\n      stroke=\"currentColor\"\n      strokeWidth=\"2.5\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      initial={false}\n      animate={{ pathLength: shown ? 1 : 0, opacity: shown ? 1 : 0 }}\n      transition={\n        reduce\n          ? { duration: 0 }\n          : shown\n            ? {\n                pathLength: { duration: duration.base, ease: ease.out, delay: duration.instant },\n                opacity: { duration: 0, delay: duration.instant },\n              }\n            : { duration: duration.fast, ease: ease.in }\n      }\n    />\n  );\n}\n\nexport type CheckboxProps = ComponentProps<typeof CheckboxPrimitive.Root> &\n  VariantProps<typeof checkboxVariants>;\n\n/**\n * A checkbox for choices submitted with a form, built on Radix Checkbox (role=\"checkbox\", Space toggles).\n * Set `checked` to \"indeterminate\" for a select-all box when only some are picked. Give it a visible label\n * (`<label htmlFor>`).\n */\nfunction Checkbox({ className, size, checked, defaultChecked, onCheckedChange, ...props }: CheckboxProps) {\n  const [uncontrolled, setUncontrolled] = useState(defaultChecked ?? false);\n  const state = checked ?? uncontrolled;\n  const reduce = useReducedMotion() ?? false;\n\n  return (\n    <CheckboxPrimitive.Root\n      data-slot=\"checkbox\"\n      data-size={size ?? \"md\"}\n      checked={state}\n      onCheckedChange={(value) => {\n        if (checked === undefined) setUncontrolled(value);\n        onCheckedChange?.(value);\n      }}\n      className={cn(checkboxVariants({ size }), className)}\n      {...props}\n    >\n      <svg data-slot=\"checkbox-indicator\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden className=\"size-full\">\n        <Mark d={CHECK} shown={state === true} reduce={reduce} />\n        <Mark d={DASH} shown={state === \"indeterminate\"} reduce={reduce} />\n      </svg>\n    </CheckboxPrimitive.Root>\n  );\n}\n\nexport { Checkbox, checkboxVariants };\n"
    }
  ],
  "categories": [
    "forms"
  ],
  "docs": "Usage and examples: https://ui.fewtone.com/components/checkbox"
}
