{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "checkbox",
  "title": "Checkbox",
  "description": "Base UI checkbox with a hand-drawn checkmark that draws on and off.",
  "dependencies": [
    "@base-ui/react",
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "springs"
  ],
  "files": [
    {
      "path": "src/components/ui/checkbox.tsx",
      "content": "\"use client\";\n\nimport {\n  Children,\n  cloneElement,\n  createContext,\n  forwardRef,\n  isValidElement,\n  useCallback,\n  useContext,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n  type HTMLAttributes,\n  type ReactElement,\n  type ReactNode,\n} from \"react\";\nimport { Checkbox as CheckboxPrimitive } from \"@base-ui/react/checkbox\";\nimport { CheckboxGroup as CheckboxGroupPrimitive } from \"@base-ui/react/checkbox-group\";\nimport { motion, AnimatePresence, useReducedMotion } from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { spring } from \"@/lib/springs\";\nimport { ProximityHoverPill } from \"@/components/ui/proximity-hover-pill\";\nimport { useProximityHover } from \"@/hooks/use-proximity-hover\";\nimport { useMergeSplit } from \"@/hooks/use-merge-split\";\n\ntype CheckboxProps = Omit<CheckboxPrimitive.Root.Props, \"inputRef\">;\n\nconst Checkbox = forwardRef<HTMLElement, CheckboxProps>(\n  (\n    {\n      className,\n      checked: checkedProp,\n      defaultChecked,\n      onCheckedChange,\n      disabled = false,\n      indeterminate = false,\n      ...props\n    },\n    ref,\n  ) => {\n    const [uncontrolledChecked, setUncontrolledChecked] = useState(defaultChecked ?? false);\n    const checked = checkedProp ?? uncontrolledChecked;\n\n    const applyChecked = (\n      next: boolean,\n      eventDetails: CheckboxPrimitive.Root.ChangeEventDetails,\n    ) => {\n      if (checkedProp === undefined) setUncontrolledChecked(next);\n      onCheckedChange?.(next, eventDetails);\n    };\n\n    const hasMountedRef = useRef(false);\n    useEffect(() => {\n      hasMountedRef.current = true;\n    }, []);\n\n    const reduceMotion = useReducedMotion();\n    const markEnterTransition =\n      reduceMotion || !hasMountedRef.current\n        ? { duration: 0 }\n        : { ...spring.quick.enter, delay: 0.06 };\n    const markExitTransition = reduceMotion ? { duration: 0 } : spring.fast.exit;\n\n    return (\n      <CheckboxPrimitive.Root\n        ref={ref}\n        data-slot=\"checkbox\"\n        checked={checked}\n        disabled={disabled}\n        indeterminate={indeterminate}\n        onCheckedChange={applyChecked}\n        className={cn(\n          \"relative flex size-4 shrink-0 items-center justify-center rounded-[5px] border border-input bg-transparent outline-none transition-colors duration-fast hover:border-foreground/40 focus-visible:ring-3 focus-visible:ring-ring/50 data-checked:border-primary data-checked:bg-primary data-checked:hover:bg-primary/90 data-indeterminate:border-primary data-indeterminate:bg-primary data-disabled:cursor-not-allowed data-disabled:opacity-50 data-disabled:hover:border-input aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40\",\n          className,\n        )}\n        {...props}\n      >\n        <CheckboxPrimitive.Indicator\n          keepMounted\n          data-slot=\"checkbox-indicator\"\n          className=\"pointer-events-none flex items-center justify-center text-primary-foreground\"\n        >\n          <AnimatePresence initial={false}>\n            {indeterminate ? (\n              <motion.svg\n                key=\"indeterminate\"\n                viewBox=\"0 0 24 24\"\n                className=\"size-3\"\n                fill=\"none\"\n                stroke=\"currentColor\"\n                strokeWidth={3}\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n              >\n                <motion.path\n                  d=\"M5 12H19\"\n                  initial={{ pathLength: hasMountedRef.current ? 0 : 1 }}\n                  animate={{ pathLength: 1, transition: markEnterTransition }}\n                  exit={{ pathLength: 0, transition: markExitTransition }}\n                />\n              </motion.svg>\n            ) : (\n              checked && (\n                <motion.svg\n                  key=\"check\"\n                  viewBox=\"0 0 24 24\"\n                  className=\"size-3\"\n                  fill=\"none\"\n                  stroke=\"currentColor\"\n                  strokeWidth={3}\n                  strokeLinecap=\"round\"\n                  strokeLinejoin=\"round\"\n                >\n                  <motion.path\n                    d=\"M4 12L9 17L20 6\"\n                    // Reads hasMountedRef during render (not inside an\n                    // effect): the very first render of an already-checked\n                    // item happens before the mount effect below has had a\n                    // chance to flip the ref, so it starts fully drawn\n                    // (pathLength 1) instead of drawing in from 0.\n                    initial={{ pathLength: hasMountedRef.current ? 0 : 1 }}\n                    animate={{\n                      pathLength: 1,\n                      transition: markEnterTransition,\n                    }}\n                    exit={{ pathLength: 0, transition: markExitTransition }}\n                  />\n                </motion.svg>\n              )\n            )}\n          </AnimatePresence>\n        </CheckboxPrimitive.Indicator>\n      </CheckboxPrimitive.Root>\n    );\n  },\n);\nCheckbox.displayName = \"Checkbox\";\n\n// ─── Contexts (CheckboxGroup) ─────────────────────────────────────────────────\n\ninterface CheckboxGroupContextValue {\n  registerItem: (index: number, element: HTMLElement | null) => void;\n  registerName: (index: number, name: string | null) => void;\n  activeIndex: number | null;\n  disabled: boolean;\n  value: string[];\n}\n\nconst CheckboxGroupContext = createContext<CheckboxGroupContextValue | null>(null);\n\nfunction useCheckboxGroupContext() {\n  const ctx = useContext(CheckboxGroupContext);\n  if (!ctx) throw new Error(\"CheckboxGroupItem must be used within a CheckboxGroup\");\n  return ctx;\n}\n\ntype CheckboxGroupProps = Omit<CheckboxGroupPrimitive.Props, \"children\"> & {\n  children: ReactNode;\n};\n\nconst CheckboxGroup = forwardRef<HTMLDivElement, CheckboxGroupProps>(\n  (\n    {\n      children,\n      className,\n      value: valueProp,\n      defaultValue,\n      onValueChange,\n      disabled = false,\n      ...rest\n    },\n    ref,\n  ) => {\n    const containerRef = useRef<HTMLDivElement>(null);\n    const [uncontrolledValue, setUncontrolledValue] = useState<string[]>(defaultValue ?? []);\n    const value = valueProp ?? uncontrolledValue;\n\n    const namesRef = useRef<Map<number, string>>(new Map());\n    const [namesTick, setNamesTick] = useState(0);\n\n    const { activeIndex, itemRects, sessionRef, handlers, registerItem, measureItems } =\n      useProximityHover(containerRef);\n\n    const registerName = useCallback((index: number, name: string | null) => {\n      if (name !== null) namesRef.current.set(index, name);\n      else namesRef.current.delete(index);\n      setNamesTick((t) => t + 1);\n    }, []);\n\n    useEffect(() => {\n      measureItems();\n    }, [measureItems, children]);\n\n    const checkedIndices = useMemo(() => {\n      const indices: number[] = [];\n      namesRef.current.forEach((name, index) => {\n        if (value.includes(name)) indices.push(index);\n      });\n      return indices;\n      // namesTick invalidates the memo when the (ref-backed) names map\n      // changes shape, since the map itself isn't a stable dependency.\n      // eslint-disable-next-line react-hooks/exhaustive-deps\n    }, [value, namesTick]);\n\n    const { blocks, change } = useMergeSplit(checkedIndices, itemRects, activeIndex);\n    const reduceMotion = useReducedMotion();\n\n    const handleValueChange = useCallback(\n      (next: string[], eventDetails: CheckboxGroupPrimitive.ChangeEventDetails) => {\n        if (valueProp === undefined) setUncontrolledValue(next);\n        onValueChange?.(next, eventDetails);\n      },\n      [valueProp, onValueChange],\n    );\n\n    // Scopes arrow-key row navigation to `[data-proximity-index]` row\n    // wrappers rather than the inner `role=\"checkbox\"` element — mirrors\n    // Accordion's `data-proximity-index` on AccordionItem.\n    const handleKeyDown = useCallback((e: React.KeyboardEvent<HTMLDivElement>) => {\n      if (e.key !== \"ArrowDown\" && e.key !== \"ArrowUp\") return;\n      const container = containerRef.current;\n      if (!container) return;\n      const row = (e.target as HTMLElement).closest<HTMLElement>(\"[data-proximity-index]\");\n      if (!row || !container.contains(row)) return;\n\n      const rows = Array.from(\n        container.querySelectorAll<HTMLElement>(\"[data-proximity-index]\"),\n      ).toSorted((a, b) => Number(a.dataset.proximityIndex) - Number(b.dataset.proximityIndex));\n      const currentPos = rows.indexOf(row);\n      if (currentPos === -1) return;\n      const nextRow = rows[e.key === \"ArrowDown\" ? currentPos + 1 : currentPos - 1];\n      if (!nextRow) return;\n\n      e.preventDefault();\n      nextRow.querySelector<HTMLElement>('[role=\"checkbox\"]')?.focus();\n    }, []);\n\n    // Memoized: the group re-renders on every proximity-hover mousemove; a\n    // fresh context object each time would re-render every item with it.\n    const contextValue = useMemo<CheckboxGroupContextValue>(\n      () => ({ registerItem, registerName, activeIndex, disabled, value }),\n      [registerItem, registerName, activeIndex, disabled, value],\n    );\n\n    // Auto-index children by position, same as Accordion's indexedChildren —\n    // callers never hand-thread an `index` prop just to get proximity hover.\n    const indexedChildren = Children.map(children, (child, position) => {\n      if (!isValidElement(child)) return child;\n      const el = child as ReactElement<{ index?: number }>;\n      return el.props.index !== undefined ? el : cloneElement(el, { index: position });\n    });\n\n    return (\n      <CheckboxGroupContext.Provider value={contextValue}>\n        <CheckboxGroupPrimitive\n          value={value}\n          onValueChange={handleValueChange}\n          disabled={disabled}\n          ref={(node: HTMLDivElement | null) => {\n            (containerRef as React.MutableRefObject<HTMLDivElement | null>).current = node;\n            if (typeof ref === \"function\") ref(node);\n            else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node;\n          }}\n          data-slot=\"checkbox-group\"\n          onMouseEnter={handlers.onMouseEnter}\n          onMouseMove={handlers.onMouseMove}\n          onMouseLeave={handlers.onMouseLeave}\n          onKeyDown={handleKeyDown}\n          className={cn(\"relative flex w-full flex-col gap-0.5\", className)}\n          {...rest}\n        >\n          {/* Merged-selection background updates to its final shape\n              immediately. The temporary overlay below supplies the local\n              absorption/release cue for the row that actually changed. */}\n          {blocks.map((block) => (\n            <div\n              key={block.key}\n              className=\"pointer-events-none absolute rounded-lg bg-accent/20 dark:bg-accent/12\"\n              style={{\n                top: block.top,\n                left: block.left,\n                width: block.width,\n                height: block.height,\n              }}\n            />\n          ))}\n\n          <AnimatePresence initial={false}>\n            {change && (\n              <motion.div\n                key={change.key}\n                className=\"pointer-events-none absolute rounded-lg bg-accent/20 dark:bg-accent/12\"\n                style={{\n                  top: change.rect.top,\n                  left: change.rect.left,\n                  width: change.rect.width,\n                  height: change.rect.height,\n                  transformOrigin: \"center\",\n                }}\n                initial={\n                  reduceMotion\n                    ? false\n                    : { opacity: change.checked ? 0 : 1, scaleY: change.checked ? 0.82 : 1 }\n                }\n                animate={{ opacity: 0, scaleY: change.checked ? 1 : 0.82 }}\n                exit={{ opacity: 0, transition: spring.fast.exit }}\n                transition={reduceMotion ? { duration: 0 } : spring.quick.enter}\n              />\n            )}\n          </AnimatePresence>\n\n          {/* Hover pill — tracks the item nearest the cursor, same faint\n              foreground-tinted wash every proximity-hover consumer uses,\n              capped below full layer-opacity so it stays subordinate to the\n              persistent merge/split background above. */}\n          <ProximityHoverPill\n            activeRect={activeIndex !== null ? (itemRects[activeIndex] ?? null) : null}\n            sessionKey={sessionRef.current}\n          />\n\n          {indexedChildren}\n        </CheckboxGroupPrimitive>\n      </CheckboxGroupContext.Provider>\n    );\n  },\n);\nCheckboxGroup.displayName = \"CheckboxGroup\";\n\n// ─── CheckboxGroupItem ───────────────────────────────────────────────────────\n// A row wrapping Checkbox — the whole row is the proximity-hover/merge-split\n// unit (data-proximity-index), same shape as AccordionItem wrapping\n// AccordionTrigger/AccordionContent.\n\ninterface CheckboxGroupItemProps extends Omit<HTMLAttributes<HTMLLabelElement>, \"onChange\"> {\n  /** Identifies this row within the group — passed straight through to the inner Checkbox's `name`. */\n  name: string;\n  /** Position for proximity hover/merge-split — auto-assigned from child order; pass explicitly only to override it. */\n  index?: number;\n  disabled?: boolean;\n  children: ReactNode;\n}\n\nconst CheckboxGroupItem = forwardRef<HTMLLabelElement, CheckboxGroupItemProps>(\n  ({ name, index, disabled = false, children, className, ...props }, ref) => {\n    const ctx = useCheckboxGroupContext();\n    const internalRef = useRef<HTMLLabelElement>(null);\n\n    useEffect(() => {\n      if (index === undefined) return;\n      ctx.registerItem(index, internalRef.current);\n      return () => ctx.registerItem(index, null);\n    }, [index, ctx]);\n\n    useEffect(() => {\n      if (index === undefined) return;\n      ctx.registerName(index, name);\n      return () => ctx.registerName(index, null);\n    }, [index, ctx, name]);\n\n    return (\n      // eslint-disable-next-line jsx-a11y/label-has-associated-control -- Checkbox renders a real hidden <input> beside its visual span, so wrapping it in <label> does associate a control; the rule can't see through the component boundary to confirm it.\n      <label\n        ref={(node: HTMLLabelElement | null) => {\n          (internalRef as React.MutableRefObject<HTMLLabelElement | null>).current = node;\n          if (typeof ref === \"function\") ref(node);\n          else if (ref) (ref as React.MutableRefObject<HTMLLabelElement | null>).current = node;\n        }}\n        data-slot=\"checkbox-group-item\"\n        data-proximity-index={index}\n        className={cn(\n          \"relative z-10 flex w-full items-center gap-2.5 rounded-lg px-3 py-2 select-none\",\n          disabled || ctx.disabled ? \"cursor-not-allowed opacity-50\" : \"cursor-pointer\",\n          className,\n        )}\n        {...props}\n      >\n        <Checkbox name={name} checked={ctx.value.includes(name)} disabled={disabled} />\n        <span className=\"flex-1 text-body text-foreground\">{children}</span>\n      </label>\n    );\n  },\n);\nCheckboxGroupItem.displayName = \"CheckboxGroupItem\";\n\nexport { Checkbox, CheckboxGroup, CheckboxGroupItem };\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}