{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "checkbox",
  "type": "registry:ui",
  "title": "Checkbox",
  "description": "Draws its tick, bends it into a dash when mixed, squashes on press.",
  "categories": [
    "selection"
  ],
  "dependencies": [
    "@base-ui/react",
    "motion"
  ],
  "registryDependencies": [
    "https://www.stealth.pm/r/tokens.json",
    "https://www.stealth.pm/r/cn.json",
    "https://www.stealth.pm/r/motion.json"
  ],
  "files": [
    {
      "path": "components/ui/checkbox.tsx",
      "content": "\"use client\";\nimport { Checkbox as BaseCheckbox } from \"@base-ui/react/checkbox\";\nimport { CheckboxGroup as BaseCheckboxGroup } from \"@base-ui/react/checkbox-group\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { useId, useState } from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { ease, spring } from \"@/lib/motion\";\n\n// The tick and the dash share one path with the same three points, so moving\n// between checked and mixed bends the stroke instead of swapping icons.\nconst CHECK = \"M3.75 8.25 L6.75 11.25 L12.25 4.75\";\nconst DASH = \"M4.25 8 L8 8 L11.75 8\";\n\ntype Shape = \"check\" | \"dash\" | \"none\";\n\nexport type CheckboxProps = Omit<BaseCheckbox.Root.Props, \"className\" | \"children\"> & {\n  /** Visible label. The whole label is the hit target. */\n  label?: React.ReactNode;\n  /** A quieter line under the label, linked as the description. */\n  description?: React.ReactNode;\n  size?: \"sm\" | \"md\";\n  /** Marks the box as invalid. Set it after a failed submit, not while the person is still deciding. */\n  invalid?: boolean;\n  /** Applied to the outermost element: the label when there is one, else the box. */\n  className?: string;\n};\n\nexport function Checkbox({\n  label,\n  description,\n  size = \"md\",\n  invalid = false,\n  className,\n  \"aria-describedby\": describedBy,\n  ...rest\n}: CheckboxProps) {\n  const descriptionId = useId();\n  const labeled = label != null;\n\n  const box = (\n    <BaseCheckbox.Root\n      data-size={size}\n      data-invalid={invalid || undefined}\n      aria-invalid={invalid || undefined}\n      aria-describedby={cn(describedBy, description != null && descriptionId) || undefined}\n      className={(state) => {\n        const filled = state.checked || state.indeterminate;\n        const bad = invalid || state.valid === false;\n        const idle = !state.disabled && !state.readOnly;\n        return cn(\n          \"relative inline-grid shrink-0 place-items-center border outline-none\",\n          \"focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-fg-3\",\n          \"transition-[background-color,border-color,color,scale] duration-150 ease-out-expo motion-reduce:transition-none\",\n          size === \"sm\" ? \"size-3.5 rounded-[4px]\" : \"size-4 rounded-[4.5px]\",\n          // Squash on press. The label shares the target, so pressing the words squashes the box too.\n          idle && \"motion-safe:active:scale-[0.86] active:duration-100 motion-safe:group-active/checkbox:scale-[0.86] group-active/checkbox:duration-100\",\n          filled\n            ? \"border-fg bg-fg text-frame\"\n            : bad\n              ? \"border-danger bg-danger-soft\"\n              : cn(\"border-fg-4 bg-raised\", idle && \"hover:border-fg-3 group-hover/checkbox:border-fg-3\"),\n          // Touch: without a label the box alone is the target, so grow it to 44px.\n          !labeled && \"after:absolute after:content-['']\",\n          !labeled && (size === \"sm\" ? \"after:-inset-[15px]\" : \"after:-inset-3.5\"),\n          !labeled && state.disabled && \"opacity-50\",\n          !labeled && className,\n        );\n      }}\n      {...rest}\n    >\n      <BaseCheckbox.Indicator\n        keepMounted\n        className=\"pointer-events-none grid place-items-center\"\n        render={(props, state) => (\n          <span {...props}>\n            <Mark shape={state.indeterminate ? \"dash\" : state.checked ? \"check\" : \"none\"} />\n          </span>\n        )}\n      />\n    </BaseCheckbox.Root>\n  );\n\n  if (!labeled) return box;\n\n  return (\n    <label\n      data-size={size}\n      className={cn(\n        \"group/checkbox relative inline-flex select-none items-start\",\n        size === \"sm\" ? \"gap-2\" : \"gap-2.5\",\n        \"has-[[data-disabled]]:opacity-50\",\n        className,\n      )}\n    >\n      <span className={cn(\"flex items-center\", size === \"sm\" ? \"h-[18px]\" : \"h-5\")}>{box}</span>\n      <span className=\"flex min-w-0 flex-col gap-px\">\n        <span className={cn(\"text-fg\", size === \"sm\" ? \"text-[12.5px] leading-[18px]\" : \"text-[13px] leading-5\")}>{label}</span>\n        {description != null && (\n          <span id={descriptionId} className={cn(\"text-fg-3\", size === \"sm\" ? \"text-[12px] leading-4\" : \"text-[12.5px] leading-[18px]\")}>\n            {description}\n          </span>\n        )}\n      </span>\n    </label>\n  );\n}\n\n/** The drawn mark. Remembers the last shape so it knows whether to draw, erase or bend. */\nfunction Mark({ shape }: { shape: Shape }) {\n  const reduce = useReducedMotion();\n  const [current, setCurrent] = useState(shape);\n  const [from, setFrom] = useState<Shape>(shape);\n  if (shape !== current) {\n    setFrom(current);\n    setCurrent(shape);\n  }\n\n  const visible = shape !== \"none\";\n  // While hidden, keep the last shape so erasing doesn't bend it on the way out.\n  const d = shape === \"dash\" || (shape === \"none\" && from === \"dash\") ? DASH : CHECK;\n  const bend = from !== \"none\" && visible;\n\n  return (\n    <svg width=\"100%\" height=\"100%\" viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden focusable=\"false\" className=\"size-full\">\n      <motion.path\n        initial={false}\n        stroke=\"currentColor\"\n        strokeWidth={1.9}\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        animate={{ d, pathLength: visible ? 1 : 0, opacity: visible ? 1 : 0 }}\n        transition={\n          reduce\n            ? { duration: 0.12, d: { duration: 0 }, pathLength: { duration: 0 } }\n            : {\n                d: bend ? spring.snappy : { duration: 0 },\n                // Draw in on the expo ease-out; erase faster than it drew.\n                pathLength: visible ? { duration: 0.24, ease: ease.out, delay: 0.03 } : { duration: 0.1, ease: ease.in },\n                opacity: visible ? { duration: 0.04, delay: 0.03 } : { duration: 0.08, delay: 0.04 },\n              }\n        }\n      />\n    </svg>\n  );\n}\n\nexport type CheckboxGroupProps = Omit<BaseCheckboxGroup.Props, \"className\"> & { className?: string };\n\n/**\n * Shared state for a list of checkboxes. Give it `allValues` and a controlled\n * `value` and a Checkbox with `parent` becomes a select-all that goes mixed.\n */\nexport function CheckboxGroup({ className, ...rest }: CheckboxGroupProps) {\n  return <BaseCheckboxGroup className={cn(\"flex flex-col gap-2.5\", className)} {...rest} />;\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "https://www.stealth.pm/ui/checkbox"
}
