{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "task-checkbox",
  "title": "Task Checkbox",
  "description": "A satisfying todo checkbox row with spring fill, drawn-in check, confetti burst and animated strikethrough",
  "dependencies": [
    "framer-motion"
  ],
  "files": [
    {
      "path": "app/registry/task-checkbox/task-checkbox.tsx",
      "content": "/**\n * Spectrum UI — TaskCheckbox\n *\n * A deeply satisfying todo checkbox row. Checking fills the box with a snappy\n * spring, draws the check in, pops the box and fires a tiny confetti burst\n * while a strikethrough sweeps across the label; unchecking reverses quickly\n * with no fanfare. Works controlled or uncontrolled, honors\n * prefers-reduced-motion, and exposes real checkbox semantics via\n * role=\"checkbox\" and aria-checked.\n *\n * Dependencies: framer-motion, @/lib/utils\n *\n * @example\n * <TaskCheckbox\n *   label=\"Ship the changelog\"\n *   description=\"Blocks the Friday release\"\n *   onCheckedChange={(checked) => save(checked)}\n * />\n */\n\n\"use client\"\n\nimport React, { useCallback, useId, useState } from \"react\"\nimport { motion, useReducedMotion } from \"framer-motion\"\nimport { cn } from \"@/lib/utils\"\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\nexport interface TaskCheckboxProps {\n  /** Controlled checked state. Leave undefined for uncontrolled usage */\n  checked?: boolean\n  /** Initial checked state when uncontrolled. Default false */\n  defaultChecked?: boolean\n  /** Fires with the next checked state on every toggle */\n  onCheckedChange?: (checked: boolean) => void\n  /** Task text next to the box; struck through while checked */\n  label: React.ReactNode\n  /** Optional secondary line rendered under the label */\n  description?: React.ReactNode\n  /** Visual size of the row. Default \"md\" */\n  size?: \"sm\" | \"md\" | \"lg\"\n  /** Disables pointer and keyboard interaction */\n  disabled?: boolean\n  /** Additional classes merged onto the row wrapper */\n  className?: string\n}\n\n// ─── Constants ───────────────────────────────────────────────────────────────\n\n/** Snappy spring for micro moves (box fill scaling in) */\nconst MICRO_SPRING = { type: \"spring\", stiffness: 500, damping: 30 } as const\n/** Ease for line reveals (label strikethrough) */\nconst REVEAL_EASE = [0.22, 1, 0.36, 1] as const\n\n/** Check stroke starts drawing shortly after the fill lands */\nconst CHECK_DRAW_DELAY = 0.06\nconst CHECK_DRAW_DURATION = 0.2\n\n/** Strikethrough grows on check, shrinks faster on uncheck */\nconst STRIKE_DURATION = 0.25\nconst UNSTRIKE_DURATION = 0.15\nconst STRIKE_HEIGHT = 1.5\n\n/** Box pop on check only — overshoot is reserved for the positive moment */\nconst POP_KEYFRAMES = [1, 0.9, 1.05, 1]\nconst POP_DURATION = 0.4\nconst POP_TIMES = [0, 0.3, 0.7, 1]\n\n/** Confetti burst fired from the box on check */\nconst CONFETTI_COUNT = 6\nconst CONFETTI_DURATION = 0.5\n/** Radial travel, as multiples of the box size (alternating far/near) */\nconst CONFETTI_DISTANCE_FAR = 1.6\nconst CONFETTI_DISTANCE_NEAR = 1.25\n/** neutral-400 and emerald-500 — emerald is the celebration accent */\nconst CONFETTI_COLORS = [\"#a3a3a3\", \"#10b981\"]\n\nconst CHECK_PATH = \"M5 12.5L10 17.5L19 7.5\"\n\nconst SIZES = {\n  sm: {\n    gap: \"gap-2.5\",\n    // 24px hit target; negative margins keep the layout footprint at box size\n    hit: \"-m-1 -mt-0.5 h-6 w-6\",\n    box: \"h-4 w-4\",\n    radius: \"rounded\",\n    boxPx: 16,\n    label: \"text-sm leading-5\",\n    description: \"text-xs leading-4\",\n  },\n  md: {\n    gap: \"gap-3\",\n    hit: \"-m-1 h-7 w-7\",\n    box: \"h-5 w-5\",\n    radius: \"rounded-md\",\n    boxPx: 20,\n    label: \"text-sm leading-5\",\n    description: \"text-xs leading-4\",\n  },\n  lg: {\n    gap: \"gap-3\",\n    hit: \"-m-1 h-8 w-8\",\n    box: \"h-6 w-6\",\n    radius: \"rounded-md\",\n    boxPx: 24,\n    label: \"text-base leading-6\",\n    description: \"text-sm leading-5\",\n  },\n} as const\n\n// ─── Component ───────────────────────────────────────────────────────────────\n\nexport function TaskCheckbox({\n  checked: checkedProp,\n  defaultChecked = false,\n  onCheckedChange,\n  label,\n  description,\n  size = \"md\",\n  disabled = false,\n  className,\n}: TaskCheckboxProps) {\n  const shouldReduceMotion = useReducedMotion()\n  const [internalChecked, setInternalChecked] = useState(defaultChecked)\n  const [burstKey, setBurstKey] = useState(0)\n  const labelId = useId()\n  const descriptionId = useId()\n\n  const checked = checkedProp ?? internalChecked\n  const sizes = SIZES[size]\n  const confettiDotSize = Math.max(3, Math.round(sizes.boxPx * 0.2))\n\n  const toggle = useCallback(() => {\n    if (disabled) return\n    const next = !checked\n    if (checkedProp === undefined) setInternalChecked(next)\n    onCheckedChange?.(next)\n    if (next && !shouldReduceMotion) setBurstKey((key) => key + 1)\n  }, [checked, checkedProp, disabled, onCheckedChange, shouldReduceMotion])\n\n  return (\n    <div\n      className={cn(\n        \"group flex select-none touch-manipulation items-start\",\n        sizes.gap,\n        disabled && \"pointer-events-none opacity-50\",\n        className,\n      )}\n    >\n      {/* Native button gives us Space/Enter toggling for free */}\n      <motion.button\n        type=\"button\"\n        role=\"checkbox\"\n        aria-checked={checked}\n        aria-labelledby={labelId}\n        aria-describedby={description ? descriptionId : undefined}\n        disabled={disabled}\n        onClick={toggle}\n        whileTap={shouldReduceMotion ? undefined : { scale: 0.9 }}\n        transition={MICRO_SPRING}\n        className={cn(\n          \"relative flex shrink-0 items-center justify-center rounded-md\",\n          \"focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-neutral-950 dark:focus-visible:ring-neutral-300\",\n          sizes.hit,\n        )}\n      >\n        <motion.span\n          className={cn(\n            \"relative flex items-center justify-center border bg-white transition-colors duration-200 dark:bg-neutral-900\",\n            checked\n              ? \"border-neutral-900 dark:border-white\"\n              : \"border-neutral-200 group-hover:border-neutral-400 dark:border-neutral-800 dark:group-hover:border-neutral-600\",\n            sizes.box,\n            sizes.radius,\n          )}\n          initial={false}\n          animate={\n            checked && !shouldReduceMotion\n              ? { scale: POP_KEYFRAMES }\n              : { scale: 1 }\n          }\n          transition={\n            checked && !shouldReduceMotion\n              ? { duration: POP_DURATION, times: POP_TIMES, ease: \"easeOut\" }\n              : { duration: 0.15 }\n          }\n        >\n          {/* Fill scales in from center under the check */}\n          <motion.span\n            aria-hidden=\"true\"\n            className={cn(\n              \"absolute inset-0 bg-neutral-900 dark:bg-white\",\n              sizes.radius,\n            )}\n            initial={false}\n            animate={{ scale: checked ? 1 : 0 }}\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : checked\n                  ? MICRO_SPRING\n                  : { duration: 0.15, ease: \"easeOut\" }\n            }\n          />\n\n          {/* Check draws in just after the fill lands; stroke inverts the fill */}\n          <svg\n            viewBox=\"0 0 24 24\"\n            className=\"relative h-full w-full p-[15%] text-white dark:text-neutral-900\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeWidth=\"3\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            aria-hidden=\"true\"\n          >\n            <motion.path\n              d={CHECK_PATH}\n              initial={false}\n              animate={{\n                pathLength: checked ? 1 : 0,\n                opacity: checked ? 1 : 0,\n              }}\n              transition={\n                shouldReduceMotion\n                  ? { duration: 0 }\n                  : checked\n                    ? {\n                        pathLength: {\n                          delay: CHECK_DRAW_DELAY,\n                          duration: CHECK_DRAW_DURATION,\n                          ease: \"easeOut\",\n                        },\n                        opacity: { delay: CHECK_DRAW_DELAY, duration: 0.01 },\n                      }\n                    : { duration: 0.1 }\n              }\n            />\n          </svg>\n\n          {/* Confetti burst — remounts per check via burstKey, unmounts on completion */}\n          {burstKey > 0 && !shouldReduceMotion && (\n            <span\n              aria-hidden=\"true\"\n              className=\"pointer-events-none absolute inset-0 flex items-center justify-center\"\n            >\n              {Array.from({ length: CONFETTI_COUNT }).map((_, index) => {\n                const angle =\n                  (index / CONFETTI_COUNT) * Math.PI * 2 - Math.PI / 2\n                const distance =\n                  sizes.boxPx *\n                  (index % 2 === 0\n                    ? CONFETTI_DISTANCE_FAR\n                    : CONFETTI_DISTANCE_NEAR)\n                return (\n                  <motion.span\n                    key={`confetti-${burstKey}-${index}`}\n                    className=\"absolute rounded-full\"\n                    style={{\n                      width: confettiDotSize,\n                      height: confettiDotSize,\n                      backgroundColor:\n                        CONFETTI_COLORS[index % CONFETTI_COLORS.length],\n                    }}\n                    initial={{ x: 0, y: 0, scale: 0, opacity: 1 }}\n                    animate={{\n                      x: Math.cos(angle) * distance,\n                      y: Math.sin(angle) * distance,\n                      scale: [0, 1, 0.3],\n                      opacity: [1, 1, 0],\n                    }}\n                    transition={{\n                      duration: CONFETTI_DURATION,\n                      ease: \"easeOut\",\n                    }}\n                    onAnimationComplete={\n                      index === CONFETTI_COUNT - 1\n                        ? () => setBurstKey(0)\n                        : undefined\n                    }\n                  />\n                )\n              })}\n            </span>\n          )}\n        </motion.span>\n      </motion.button>\n\n      {/* Text is a click target too; the button carries the semantics */}\n      <div\n        onClick={toggle}\n        className={cn(\"flex min-w-0 flex-col\", !disabled && \"cursor-pointer\")}\n      >\n        <span\n          id={labelId}\n          className={cn(\n            \"relative w-fit transition-colors\",\n            checked\n              ? \"text-neutral-400 duration-200 dark:text-neutral-600\"\n              : \"text-neutral-900 duration-150 dark:text-neutral-100\",\n            sizes.label,\n          )}\n        >\n          {label}\n          {/* Strikethrough grows from the left on check, retreats right on uncheck */}\n          <motion.span\n            aria-hidden=\"true\"\n            className=\"absolute left-0 right-0 bg-neutral-400 dark:bg-neutral-600\"\n            style={{\n              height: STRIKE_HEIGHT,\n              top: `calc(50% - ${STRIKE_HEIGHT / 2}px)`,\n              transformOrigin: checked ? \"0% 50%\" : \"100% 50%\",\n            }}\n            initial={false}\n            animate={{ scaleX: checked ? 1 : 0 }}\n            transition={\n              shouldReduceMotion\n                ? { duration: 0 }\n                : {\n                    duration: checked ? STRIKE_DURATION : UNSTRIKE_DURATION,\n                    ease: REVEAL_EASE,\n                  }\n            }\n          />\n        </span>\n        {description && (\n          <span\n            id={descriptionId}\n            className={cn(\n              \"mt-0.5 transition-colors duration-200\",\n              checked\n                ? \"text-neutral-400 dark:text-neutral-700\"\n                : \"text-neutral-500 dark:text-neutral-400\",\n              sizes.description,\n            )}\n          >\n            {description}\n          </span>\n        )}\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/spectrumui/task-checkbox.tsx"
    }
  ],
  "type": "registry:component"
}
