{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "bounce-checkbox",
  "title": "Bounce Checkbox",
  "author": "henriquegpb <https://github.com/henriquegpb>",
  "description": "A checkbox whose fill springs out past the box with a little swell, draws its tick, then strikes the label through and dims it.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/microkit/bounce-checkbox.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useLayoutEffect, useRef, useState } from \"react\";\nimport type { CSSProperties, PointerEvent, ReactNode } from \"react\";\nimport { animate, useMotionValue, useMotionValueEvent, useReducedMotion } from \"motion/react\";\n\nexport type StrikeSide = \"left\" | \"center\" | \"right\" | \"none\";\n\nexport type BounceCheckboxProps = {\n  label?: ReactNode;\n  checked?: boolean;\n  defaultChecked?: boolean;\n  onChange?: (checked: boolean) => void;\n  disabled?: boolean;\n  color?: string;\n  fillColor?: string;\n  checkColor?: string;\n  boxSize?: number;\n  boxRadius?: number;\n  fontSize?: number;\n  bounce?: number;\n  strikeLag?: number;\n  doneOpacity?: number;\n  strike?: StrikeSide;\n  ariaLabel?: string;\n  className?: string;\n};\n\nconst VISUAL_DURATION = 0.2;\nconst RULE_END = 0.84;\nconst SWELL = 0.35;\nconst TICK_PATH = \"M5 14L8.5 17.5L19 6.5\";\nconst ORIGIN: Record<StrikeSide, string> = {\n  left: \"left center\",\n  center: \"center\",\n  right: \"right center\",\n  none: \"left center\",\n};\n\nconst clamp01 = (value: number) => Math.min(1, Math.max(0, value));\nconst zetaOf = (bounce: number) =>\n  bounce <= 0 ? 1 : -Math.log(bounce) / Math.sqrt(Math.PI ** 2 + Math.log(bounce) ** 2);\n\nconst readings = (t: number, doneOpacity: number, strikeLag: number) => {\n  const held = clamp01(t);\n  return {\n    fill: `scale(${Math.max(t, 0)})`,\n    box: `scale(${1 + SWELL * Math.max(0, t - 1)})`,\n    tick: 1 - held,\n    word: 1 - (1 - doneOpacity) * held,\n    rule: `scaleX(${clamp01((held - strikeLag) / (RULE_END - strikeLag))})`,\n  };\n};\n\nexport function BounceCheckbox({\n  label = \"Write the changelog\",\n  checked,\n  defaultChecked = false,\n  onChange,\n  disabled = false,\n  color = \"light-dark(#14181e, #f2f4f6)\",\n  fillColor = \"light-dark(#14181e, #f2f4f6)\",\n  checkColor = \"light-dark(#ffffff, #0b0b0f)\",\n  boxSize = 28,\n  boxRadius = 9,\n  fontSize = 18,\n  bounce = 0.2,\n  strikeLag = 0.12,\n  doneOpacity = 0.42,\n  strike = \"left\",\n  ariaLabel,\n  className = \"\",\n}: BounceCheckboxProps) {\n  const controlled = checked !== undefined;\n  const [inner, setInner] = useState(defaultChecked);\n  const on = controlled ? checked : inner;\n  const reduce = useReducedMotion();\n\n  const t = useMotionValue(on ? 1 : 0);\n  const viaPointer = useRef(false);\n  const instant = useRef(false);\n  const rowRef = useRef<HTMLButtonElement>(null);\n  const boxRef = useRef<HTMLSpanElement>(null);\n  const fillRef = useRef<HTMLSpanElement>(null);\n  const tickRef = useRef<SVGPathElement>(null);\n  const wordRef = useRef<HTMLSpanElement>(null);\n  const ruleRef = useRef<HTMLSpanElement>(null);\n  const cfg = useRef({ doneOpacity, strikeLag });\n\n  useLayoutEffect(() => {\n    cfg.current = { doneOpacity, strikeLag };\n  });\n\n  const write = (value: number) => {\n    const r = readings(value, cfg.current.doneOpacity, cfg.current.strikeLag);\n    if (fillRef.current) fillRef.current.style.transform = r.fill;\n    if (boxRef.current) boxRef.current.style.transform = r.box;\n    if (tickRef.current) tickRef.current.style.strokeDashoffset = String(r.tick);\n    if (wordRef.current) wordRef.current.style.opacity = String(r.word);\n    if (ruleRef.current) ruleRef.current.style.transform = r.rule;\n  };\n  useMotionValueEvent(t, \"change\", write);\n  useLayoutEffect(() => {\n    write(t.get());\n  });\n\n  useEffect(() => {\n    const target = on ? 1 : 0;\n    if (reduce || instant.current) {\n      instant.current = false;\n      t.jump(target);\n      return undefined;\n    }\n    if (t.get() === target && t.getVelocity() === 0) return undefined;\n    const controls = animate(t, target, {\n      type: \"spring\",\n      visualDuration: VISUAL_DURATION,\n      bounce: 1 - zetaOf(bounce),\n    });\n    return () => controls.stop();\n  }, [on, reduce, bounce, t]);\n\n  const handlePointerDown = (event: PointerEvent<HTMLButtonElement>) => {\n    if (event.button !== 0 || disabled) return;\n    viaPointer.current = true;\n    if (!reduce && rowRef.current) rowRef.current.dataset.pressed = \"\";\n  };\n  const handlePointerUp = () => {\n    if (rowRef.current) delete rowRef.current.dataset.pressed;\n  };\n  const handlePointerCancel = () => {\n    viaPointer.current = false;\n    handlePointerUp();\n  };\n  const toggle = () => {\n    if (disabled) return;\n    instant.current = !viaPointer.current;\n    viaPointer.current = false;\n    const next = !on;\n    if (!controlled) setInner(next);\n    onChange?.(next);\n  };\n\n  const initial = readings(on ? 1 : 0, doneOpacity, strikeLag);\n  const ring = boxSize >= 24 ? 2 : 1.5;\n  const gap = Math.min(16, Math.max(8, Math.round(boxSize * 0.43)));\n  const ruleHeight = Math.max(1.5, Math.round(fontSize / 6) / 2);\n\n  const cssVars = {\n    \"--bc-ink\": color,\n    \"--bc-fill\": fillColor,\n    \"--bc-check\": checkColor,\n    \"--bc-box\": `${boxSize}px`,\n    \"--bc-radius\": `${boxRadius}px`,\n    \"--bc-font\": `${fontSize}px`,\n    \"--bc-ring\": `${ring}px`,\n    \"--bc-gap\": `${gap}px`,\n    \"--bc-row\": `${Math.max(44, boxSize + 16)}px`,\n    \"--bc-rule\": `${ruleHeight}px`,\n    \"--bc-origin\": ORIGIN[strike] || ORIGIN.left,\n  } as CSSProperties;\n\n  return (\n    <button\n      ref={rowRef}\n      type=\"button\"\n      role=\"checkbox\"\n      aria-checked={on}\n      aria-label={ariaLabel}\n      disabled={disabled}\n      className={`group relative inline-flex min-h-[var(--bc-row)] cursor-pointer touch-manipulation select-none items-center gap-[var(--bc-gap)] border-0 bg-transparent p-0 text-left text-[length:var(--bc-font)] font-medium leading-[1.2] tracking-[-.01em] outline-none [color:var(--bc-ink)] [-webkit-tap-highlight-color:transparent] [-webkit-touch-callout:none] disabled:cursor-not-allowed disabled:opacity-50${className ? ` ${className}` : \"\"}`}\n      style={cssVars}\n      onPointerDown={handlePointerDown}\n      onPointerUp={handlePointerUp}\n      onPointerCancel={handlePointerCancel}\n      onPointerLeave={handlePointerCancel}\n      onClick={toggle}\n    >\n      <span className=\"h-[var(--bc-box)] w-[var(--bc-box)] flex-none rounded-[var(--bc-radius)] [transition:transform_160ms_cubic-bezier(.23,1,.32,1)] group-data-[pressed]:[transform:scale(.95)] group-focus-visible:outline-offset-[3px] group-focus-visible:[outline:2px_solid_color-mix(in_srgb,var(--bc-ink)_45%,transparent)] motion-reduce:transition-none\">\n        <span ref={boxRef} className=\"relative grid h-full w-full origin-center place-items-center overflow-hidden rounded-[inherit]\" style={{ transform: initial.box }}>\n          <span\n            className=\"absolute inset-0 rounded-[inherit] opacity-[.28] [box-shadow:inset_0_0_0_var(--bc-ring)_var(--bc-ink)] [transition:opacity_120ms_ease] [@media(hover:hover)_and_(pointer:fine)]:group-enabled:group-hover:opacity-50\"\n            aria-hidden=\"true\"\n          />\n          <span ref={fillRef} className=\"absolute inset-0 origin-center rounded-[inherit] [background:var(--bc-fill)]\" style={{ transform: initial.fill }} />\n          <svg\n            className=\"relative h-[68%] w-[68%] overflow-visible fill-none [stroke:var(--bc-check)] [stroke-width:2.6] [stroke-linecap:round] [stroke-linejoin:round]\"\n            viewBox=\"0 0 24 24\"\n            aria-hidden=\"true\"\n          >\n            <path ref={tickRef} d={TICK_PATH} pathLength={1} strokeDasharray={1} style={{ strokeDashoffset: initial.tick }} />\n          </svg>\n        </span>\n      </span>\n      <span className=\"relative inline-block\">\n        <span ref={wordRef} className=\"inline-block\" style={{ opacity: initial.word }}>\n          {label}\n        </span>\n        {strike !== \"none\" ? (\n          <span\n            ref={ruleRef}\n            className=\"pointer-events-none absolute inset-x-0 top-[46%] h-[var(--bc-rule)] rounded-[2px] bg-current [transform-origin:var(--bc-origin)]\"\n            aria-hidden=\"true\"\n            style={{ transform: initial.rule }}\n          />\n        ) : null}\n      </span>\n    </button>\n  );\n}\n\nexport function BounceCheckboxList() {\n  return (\n    <div className=\"flex flex-col items-start gap-1\">\n      <BounceCheckbox label=\"Write the changelog\" />\n      <BounceCheckbox label=\"Publish the release\" />\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/microkit/bounce-checkbox.tsx"
    }
  ],
  "docs": "Live preview and the CSS, JavaScript and Tailwind variants: https://microkit.co/components/bounce-checkbox",
  "categories": [
    "input",
    "click"
  ],
  "type": "registry:component"
}