{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"motion-checkbox","type":"registry:component","title":"Checkbox (Motion)","description":"Checkbox with motion: the tick draws itself along its stroke as it's checked and back out as it's cleared, and the box presses in on a spring.","dependencies":["motion","radix-ui"],"registryDependencies":["https://dashral.com/r/utils.json","https://dashral.com/r/motion-field.json","https://dashral.com/r/motion-reduced.json","https://dashral.com/r/motion-tokens.json","https://dashral.com/r/checkbox.json","https://dashral.com/r/field.json"],"files":[{"path":"registry/dashral/motion/checkbox.tsx","type":"registry:file","target":"components/dashral/motion/checkbox.tsx","content":"\"use client\"\n\nimport * as React from \"react\"\nimport { Checkbox as CheckboxPrimitive } from \"radix-ui\"\nimport { motion } from \"motion/react\"\nimport { ChoiceField, joinIds } from \"../field\"\nimport type { CheckboxProps, ControlColor } from \"../checkbox\"\nimport { cn } from \"../lib/utils\"\n\nimport { motionMessages } from \"../lib/motion-field\"\nimport { useReducedMotion } from \"../lib/motion-reduced\"\nimport { duration, ease, spring } from \"../lib/motion-tokens\"\n\nexport type { CheckboxProps } from \"../checkbox\"\n\ntype CheckedState = boolean | \"indeterminate\"\n\n// Ticked: filled with the color, as the kit's Checkbox has it. Every class is written out in full so Tailwind finds it.\nconst solidColors: Record<ControlColor, string> = {\n  primary: \"data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=indeterminate]:border-primary data-[state=indeterminate]:bg-primary data-[state=indeterminate]:text-primary-foreground\",\n  neutral: \"data-[state=checked]:border-foreground data-[state=checked]:bg-foreground data-[state=checked]:text-background data-[state=indeterminate]:border-foreground data-[state=indeterminate]:bg-foreground data-[state=indeterminate]:text-background\",\n  info: \"data-[state=checked]:border-info data-[state=checked]:bg-info data-[state=checked]:text-info-foreground data-[state=indeterminate]:border-info data-[state=indeterminate]:bg-info data-[state=indeterminate]:text-info-foreground\",\n  success: \"data-[state=checked]:border-success data-[state=checked]:bg-success data-[state=checked]:text-success-foreground data-[state=indeterminate]:border-success data-[state=indeterminate]:bg-success data-[state=indeterminate]:text-success-foreground\",\n  warning: \"data-[state=checked]:border-warning data-[state=checked]:bg-warning data-[state=checked]:text-warning-foreground data-[state=indeterminate]:border-warning data-[state=indeterminate]:bg-warning data-[state=indeterminate]:text-warning-foreground\",\n  destructive: \"data-[state=checked]:border-destructive data-[state=checked]:bg-destructive data-[state=checked]:text-destructive-foreground data-[state=indeterminate]:border-destructive data-[state=indeterminate]:bg-destructive data-[state=indeterminate]:text-destructive-foreground\",\n  highlight: \"data-[state=checked]:border-highlight data-[state=checked]:bg-highlight data-[state=checked]:text-highlight-foreground data-[state=indeterminate]:border-highlight data-[state=indeterminate]:bg-highlight data-[state=indeterminate]:text-highlight-foreground\",\n}\n\n// Ticked: a colored border and mark, no fill. Every class is written out in full so Tailwind finds it.\nconst outlineColors: Record<ControlColor, string> = {\n  primary: \"data-[state=checked]:border-primary data-[state=checked]:text-primary data-[state=indeterminate]:border-primary data-[state=indeterminate]:text-primary\",\n  neutral: \"data-[state=checked]:border-foreground data-[state=checked]:text-foreground data-[state=indeterminate]:border-foreground data-[state=indeterminate]:text-foreground\",\n  info: \"data-[state=checked]:border-info data-[state=checked]:text-info data-[state=indeterminate]:border-info data-[state=indeterminate]:text-info\",\n  success: \"data-[state=checked]:border-success data-[state=checked]:text-success data-[state=indeterminate]:border-success data-[state=indeterminate]:text-success\",\n  warning: \"data-[state=checked]:border-warning data-[state=checked]:text-warning data-[state=indeterminate]:border-warning data-[state=indeterminate]:text-warning\",\n  destructive: \"data-[state=checked]:border-destructive data-[state=checked]:text-destructive data-[state=indeterminate]:border-destructive data-[state=indeterminate]:text-destructive\",\n  highlight: \"data-[state=checked]:border-highlight data-[state=checked]:text-highlight data-[state=indeterminate]:border-highlight data-[state=indeterminate]:text-highlight\",\n}\n\n/**\n * The kit's Checkbox with motion: the same label, description, colors, variants and props. The tick draws itself\n * along its stroke as it's checked and draws back out as it's cleared; \"indeterminate\" draws its dash from the middle\n * out; the box presses in on a spring while held. Press one and sweep the pointer over others, and each takes the\n * state the first is going to, ticks drawing in one after another down the list. With reduced motion the mark only fades.\n */\nexport function Checkbox({ label, description, error, id, disabled, labelPosition, color = \"primary\", variant = \"solid\", className, classNames, ...props }: CheckboxProps) {\n  return (\n    <ChoiceField renderMessages={motionMessages} {...{ label, description, error, id, disabled, labelPosition, classNames }} leading=\"leading-4\">\n      {(control) => (\n        <CheckboxControl\n          {...props}\n          {...control}\n          disabled={disabled}\n          aria-describedby={joinIds(props[\"aria-describedby\"], control[\"aria-describedby\"])}\n          aria-invalid={props[\"aria-invalid\"] ?? control[\"aria-invalid\"]}\n          className={cn((variant === \"outline\" ? outlineColors : solidColors)[color], className, classNames?.checkbox)}\n        />\n      )}\n    </ChoiceField>\n  )\n}\n\n/**\n * A sweep: press a checkbox and, still holding, run the pointer over others; each one it passes takes the state the\n * first is going to (all ticked, or all cleared), as in a spreadsheet or a mail list. One at a time on a page, so it\n * lives here rather than in any one checkbox.\n */\nlet sweep: { target: boolean } | null = null\n\nfunction startSweep(target: boolean) {\n  sweep = { target }\n  // Running over labels mustn't select their text along the way.\n  const select = document.body.style.userSelect\n  document.body.style.userSelect = \"none\"\n  const end = () => {\n    sweep = null\n    document.body.style.userSelect = select\n    window.removeEventListener(\"pointerup\", end)\n    window.removeEventListener(\"pointercancel\", end)\n  }\n  window.addEventListener(\"pointerup\", end)\n  window.addEventListener(\"pointercancel\", end)\n}\n\nfunction CheckboxControl({\n  className,\n  checked,\n  defaultChecked,\n  onCheckedChange,\n  onPointerDown,\n  onPointerEnter,\n  onPointerLeave,\n  onClick,\n  ...props\n}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {\n  const reduced = useReducedMotion()\n  const [own, setOwn] = React.useState<CheckedState>(defaultChecked ?? false)\n  const state = checked ?? own\n  const set = (next: CheckedState) => {\n    if (checked === undefined) setOwn(next)\n    onCheckedChange?.(next)\n  }\n  // Set by a sweep: the click this checkbox may still get (the press began here) isn't a second toggle.\n  const swept = React.useRef(false)\n  const pressedHere = React.useRef(false)\n  const takeSweep = (event: React.PointerEvent) => {\n    if (!sweep || !(event.buttons & 1) || props.disabled || swept.current) return\n    swept.current = true\n    if (state !== sweep.target) set(sweep.target)\n  }\n\n  // Drawing in eases out as it arrives; drawing back out is quicker and gathers speed, like the kit's enter and exit.\n  const draw = (on: boolean) =>\n    reduced\n      ? { opacity: on ? 1 : 0, transition: { duration: duration.fast } }\n      : {\n          pathLength: on ? 1 : 0,\n          // A path drawn to zero still shows its round cap as a dot, so it fades at the very end.\n          opacity: on ? 1 : 0,\n          transition: on\n            ? { pathLength: { duration: duration.standard, ease: ease.enter }, opacity: { duration: 0.01 } }\n            : { pathLength: { duration: duration.fast, ease: ease.exit }, opacity: { duration: 0.01, delay: duration.fast } },\n        }\n\n  return (\n    <CheckboxPrimitive.Root\n      data-slot=\"checkbox\"\n      checked={state}\n      onCheckedChange={set}\n      asChild\n      {...props}\n      onPointerDown={(event) => {\n        onPointerDown?.(event)\n        swept.current = false\n        // A mouse or pen sweep (a finger's drag scrolls the page instead).\n        if (event.button === 0 && event.pointerType !== \"touch\" && !props.disabled) {\n          pressedHere.current = true\n          startSweep(state !== true)\n        }\n      }}\n      onPointerLeave={(event) => {\n        onPointerLeave?.(event)\n        // Leaving the first one with the button down starts the sweep: it switches now, not on a click it won't get.\n        if (pressedHere.current) {\n          pressedHere.current = false\n          takeSweep(event)\n        }\n      }}\n      onPointerEnter={(event) => {\n        onPointerEnter?.(event)\n        if (!pressedHere.current) {\n          swept.current = false\n          takeSweep(event)\n        }\n      }}\n      onClick={(event) => {\n        pressedHere.current = false\n        if (swept.current) {\n          swept.current = false\n          event.preventDefault()\n        }\n        onClick?.(event)\n      }}\n    >\n      <motion.button\n        whileTap={reduced || props.disabled ? undefined : { scale: 0.85 }}\n        transition={spring.press}\n        className={cn(\n          // Colors change in CSS; transform is the press's, so CSS mustn't ease it too.\n          \"peer group size-4 shrink-0 rounded-[4px] border border-input shadow-xs transition-[color,background-color,border-color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive\",\n          className\n        )}\n      >\n        {/* Always mounted, so clearing can draw the mark back out: Radix would take it away at once. */}\n        <CheckboxPrimitive.Indicator forceMount data-slot=\"checkbox-indicator\" className=\"flex items-center justify-center text-current\">\n          <svg viewBox=\"0 0 24 24\" aria-hidden className=\"size-3.5 fill-none stroke-current stroke-2 [stroke-linecap:round] [stroke-linejoin:round]\">\n            <motion.path d=\"M4 12l5 5L20 6\" initial={false} animate={draw(state === true)} />\n            {/* The dash starts from its middle: two halves drawn outward. */}\n            <motion.path d=\"M12 12h7\" initial={false} animate={draw(state === \"indeterminate\")} />\n            <motion.path d=\"M12 12H5\" initial={false} animate={draw(state === \"indeterminate\")} />\n          </svg>\n        </CheckboxPrimitive.Indicator>\n      </motion.button>\n    </CheckboxPrimitive.Root>\n  )\n}\n"}],"cssVars":{"theme":{"color-destructive-foreground":"var(--destructive-foreground)","color-destructive-text":"var(--destructive-text)","color-info":"var(--info)","color-info-foreground":"var(--info-foreground)","color-info-text":"var(--info-text)","color-success":"var(--success)","color-success-foreground":"var(--success-foreground)","color-success-text":"var(--success-text)","color-warning":"var(--warning)","color-warning-foreground":"var(--warning-foreground)","color-warning-text":"var(--warning-text)","color-highlight":"var(--highlight)","color-highlight-foreground":"var(--highlight-foreground)","color-highlight-text":"var(--highlight-text)"},"light":{"destructive-foreground":"oklch(0.985 0 0)","destructive-text":"#bd2800","info":"oklch(0.5 0.134 242.749)","info-foreground":"oklch(0.985 0 0)","info-text":"oklch(0.443 0.11 240.79)","success":"oklch(0.508 0.118 165.612)","success-foreground":"oklch(0.985 0 0)","success-text":"oklch(0.432 0.095 166.913)","warning":"#f59200","warning-foreground":"oklch(0.3 0.07 60)","warning-text":"#964e00","highlight":"#b8008c","highlight-foreground":"oklch(0.985 0 0)","highlight-text":"#a3007b"},"dark":{"destructive-foreground":"oklch(0.2 0 0)","destructive-text":"#ff8a5c","info":"oklch(0.685 0.169 237.323)","info-foreground":"oklch(0.2 0 0)","info-text":"oklch(0.746 0.16 232.661)","success":"oklch(0.696 0.17 162.48)","success-foreground":"oklch(0.2 0.04 160)","success-text":"oklch(0.765 0.177 163.223)","warning":"#f59200","warning-foreground":"oklch(0.25 0.06 60)","warning-text":"#ffba42","highlight":"#ff2ecf","highlight-foreground":"oklch(0.2 0 0)","highlight-text":"#ff75df"}}}