{
  "slug": "task-list",
  "name": "Task List",
  "description": "Interactive todo task list featuring micro-particle spark bursts, ambient glow halos, smooth SVG ring morphing, and fluid spring layout reordering.",
  "summary": "An interactive task list with a multi-stage completion sequence: a radial particle burst, a glow halo, a dashed ring that fades out while an accent fill scales in and a checkmark draws, a strikethrough, and spring layout reordering of completed tasks.",
  "category": "display",
  "tags": [
    "task",
    "todo",
    "list",
    "checkbox",
    "particles",
    "animation",
    "motion",
    "spring"
  ],
  "dependencies": [
    "motion",
    "clsx",
    "tailwind-merge"
  ],
  "version": "1.0.0",
  "createdDate": "2026-09-22",
  "updatedDate": "2026-09-22",
  "interactive": true,
  "supportsColor": true,
  "highlights": [
    "8-point radial geometric particle spark bursts emitting on task completion",
    "Dashed SVG ring fades out as an accent fill scales in (220ms) and the checkmark draws (240ms)",
    "Background-image strikethrough that wraps across lines (box-decoration-break: clone), with dimmed text",
    "Spring layout reordering that glides completed tasks smoothly to the bottom",
    "Horizontal flick nudge (0, 6, -2, 0 px) after the strike",
    "Three calibrated density scales (sm, md, lg)"
  ],
  "anatomy": [
    "<TaskList> (Main orchestrator managing task collection and fluid reordering)",
    "<TaskItem> (Interactive row item with checkbox, label, and spring layout)",
    "<TaskCheck> (SVG multi-layer checkmark combining dashed boundary, fill ring, and path draw)",
    "<TaskParticles> (Radial micro-particle emitter firing sparks outward on completion)"
  ],
  "physics": {
    "engine": "Framer Motion Spring & GPU Keyframe Choreography",
    "description": "Multi-stage choreographed animation sequence transitioning through tick, strike, nudge, and settled stages with hardware-accelerated transforms.",
    "parameters": [
      {
        "label": "Reorder Spring",
        "value": "stiffness: 340, damping: 28"
      },
      {
        "label": "Pop Scale",
        "value": "[1, 1.12, 1] / 320ms"
      },
      {
        "label": "Flick Nudge",
        "value": "[0, 6, -2, 0] / 280ms"
      },
      {
        "label": "Strike Duration",
        "value": "360ms ease-in-out"
      },
      {
        "label": "Particle Emitter",
        "value": "8 sparks, 480ms radial ease-out"
      }
    ]
  },
  "accessibility": {
    "role": "checkbox / list",
    "aria": "Each TaskItem provides role='checkbox' and aria-checked. The task list includes an aria-live='polite' region announcing task completion status changes.",
    "reducedMotion": "Full respect for prefers-reduced-motion, instantly settling states without intermediate flick, pop, or particle sequences.",
    "keyboard": [
      {
        "key": "Tab / Shift+Tab",
        "description": "Move between tasks."
      },
      {
        "key": "Enter / Space",
        "description": "Toggle the focused task."
      }
    ]
  },
  "guidelines": {
    "recommended": [
      "Task checklists, onboarding steps, and actionable project todo workflows",
      "Feature checklists with interactive progress states in product dashboards",
      "Interactive completion steps in technical forms and wizard flows"
    ],
    "bestPractices": [
      "Pass an accent color matching your primary theme or studio palette",
      "Enable reorderCompleted for satisfying dopamine-rich task clearing",
      "Use the size prop ('sm', 'md', 'lg') to balance density across desktop and mobile screens"
    ]
  },
  "props": [
    {
      "name": "tasks",
      "type": "Task[]",
      "defaultValue": "undefined",
      "description": "Controlled list of task objects."
    },
    {
      "name": "defaultTasks",
      "type": "Task[]",
      "defaultValue": "DEFAULT_SAMPLE_TASKS",
      "description": "Uncontrolled initial tasks array."
    },
    {
      "name": "size",
      "type": "\"sm\" | \"md\" | \"lg\"",
      "defaultValue": "\"md\"",
      "description": "Controls dimensions, padding, font size, and checkmark scale."
    },
    {
      "name": "accent",
      "type": "string",
      "defaultValue": "\"#FF5F2E\"",
      "description": "Custom accent color hex for checkmark and particles."
    },
    {
      "name": "reorderCompleted",
      "type": "boolean",
      "defaultValue": "true",
      "description": "Whether completed tasks automatically glide to the bottom of the list."
    },
    {
      "name": "onTasksChange",
      "type": "(tasks: Task[]) => void",
      "defaultValue": "undefined",
      "description": "Callback invoked when tasks are toggled."
    }
  ],
  "usage": "import { TaskList } from \"@/components/ui/task-list\";\n\nconst tasks = [\n  { id: \"1\", label: \"Write release notes\" },\n  { id: \"2\", label: \"Update screenshots\", done: true },\n];\n\nexport function TaskListDemo() {\n  return <TaskList defaultTasks={tasks} />;\n}",
  "files": [
    {
      "name": "task-list.tsx",
      "path": "registry/ui/task-list.tsx",
      "code": "\"use client\";\n\nimport {\n  useState,\n  type ComponentProps,\n  type CSSProperties,\n} from \"react\";\nimport { motion, useReducedMotion, type Transition } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\nconst EASE_OUT = [0.22, 1, 0.36, 1] as const;\nconst EASE_IN_OUT = [0.65, 0, 0.35, 1] as const;\n\nconst POP_SCALE = [1, 1.12, 1];\nconst FLICK = [0, 6, -2, 0];\nconst FLICK_TIMES = [0, 0.35, 0.7, 1];\n\nconst FILL: Transition = { duration: 0.22, ease: EASE_OUT };\nconst POP: Transition = { duration: 0.32, ease: EASE_OUT, times: [0, 0.4, 1] };\nconst TICK: Transition = { duration: 0.24, ease: EASE_OUT, delay: 0.04 };\nconst STRIKE: Transition = { duration: 0.36, ease: EASE_IN_OUT };\nconst NUDGE: Transition = {\n  duration: 0.28,\n  ease: EASE_OUT,\n  times: FLICK_TIMES,\n};\nconst REORDER: Transition = { type: \"spring\", stiffness: 340, damping: 28 };\nconst INSTANT: Transition = { duration: 0 };\n\nconst RING_R = 10.5;\nconst RING_DASH = \"1 4.5\";\n\nconst STRIKE_STYLE: CSSProperties = {\n  backgroundImage: \"linear-gradient(currentColor, currentColor)\",\n  backgroundRepeat: \"no-repeat\",\n  backgroundPosition: \"0 52%\",\n  boxDecorationBreak: \"clone\",\n  WebkitBoxDecorationBreak: \"clone\",\n};\n\nconst SIZES = {\n  sm: {\n    row: \"gap-2.5 rounded-xl px-3 py-2\",\n    check: \"h-4.5 w-4.5\",\n    text: \"text-[13px] leading-5\",\n    line: \"1.5px\",\n    list: \"gap-1.5\",\n    particleDist: 14,\n  },\n  md: {\n    row: \"gap-3 rounded-[14px] px-3.5 py-2.5\",\n    check: \"h-5.5 w-5.5\",\n    text: \"text-[14.5px] leading-6\",\n    line: \"2px\",\n    list: \"gap-2\",\n    particleDist: 18,\n  },\n  lg: {\n    row: \"gap-3.5 rounded-2xl px-4 py-3\",\n    check: \"h-6.5 w-6.5\",\n    text: \"text-[16px] leading-7\",\n    line: \"2.5px\",\n    list: \"gap-2.5\",\n    particleDist: 22,\n  },\n} as const;\n\nexport type TaskSize = keyof typeof SIZES;\n\nconst STAGE = {\n  idle: \"idle\",\n  tick: \"tick\",\n  strike: \"strike\",\n  nudge: \"nudge\",\n  settled: \"settled\",\n  unstrike: \"unstrike\",\n  untick: \"untick\",\n} as const;\ntype Stage = (typeof STAGE)[keyof typeof STAGE];\n\nconst FILLED: Stage[] = [\"tick\", \"strike\", \"nudge\", \"settled\", \"unstrike\"];\nconst STRUCK: Stage[] = [\"strike\", \"nudge\", \"settled\"];\n\nconst CARD =\n  \"bg-white/80 border border-zinc-200/80 shadow-[0_2px_8px_rgba(0,0,0,0.06)] hover:border-zinc-300 hover:bg-zinc-50 active:scale-[0.985] text-zinc-900 backdrop-blur-md dark:bg-zinc-900/70 dark:border-white/8 dark:shadow-[0_2px_8px_rgba(0,0,0,0.25)] dark:hover:border-white/15 dark:hover:bg-zinc-900/90 dark:text-zinc-100\";\nconst FOCUS =\n  \"outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-white/40 dark:focus-visible:ring-offset-black\";\n\nconst PARTICLE_ANGLES = [0, 45, 90, 135, 180, 225, 270, 315] as const;\n\nfunction useTiming() {\n  const reduced = useReducedMotion() ?? false;\n  return (transition: Transition) => (reduced ? INSTANT : transition);\n}\n\nfunction TaskParticles({\n  active,\n  distance,\n  accent,\n}: {\n  active: boolean;\n  distance: number;\n  accent: string;\n}) {\n  const timing = useTiming();\n\n  return (\n    <div className=\"absolute inset-0 pointer-events-none flex items-center justify-center\">\n      {PARTICLE_ANGLES.map((angle, i) => {\n        const rad = (angle * Math.PI) / 180;\n        const targetX = Math.cos(rad) * distance;\n        const targetY = Math.sin(rad) * distance;\n        const size = i % 2 === 0 ? 3.5 : 2.5;\n\n        return (\n          <motion.span\n            key={angle}\n            className=\"absolute rounded-full\"\n            style={{\n              width: size,\n              height: size,\n              backgroundColor: accent,\n            }}\n            initial={{ x: 0, y: 0, scale: 0, opacity: 0 }}\n            animate={\n              active\n                ? {\n                    x: [0, targetX * 1.1, targetX],\n                    y: [0, targetY * 1.1, targetY],\n                    scale: [0, 1.4, 0],\n                    opacity: [1, 1, 0],\n                  }\n                : { x: 0, y: 0, scale: 0, opacity: 0 }\n            }\n            transition={\n              active\n                ? timing({\n                    duration: 0.48,\n                    ease: EASE_OUT,\n                    times: [0, 0.4, 1],\n                  })\n                : INSTANT\n            }\n          />\n        );\n      })}\n    </div>\n  );\n}\n\nfunction TaskCheck({\n  filled,\n  size,\n  onDrawn,\n  accent,\n}: {\n  filled: boolean;\n  size: TaskSize;\n  onDrawn: () => void;\n  accent: string;\n}) {\n  const timing = useTiming();\n  const dist = SIZES[size].particleDist;\n\n  return (\n    <div className=\"relative shrink-0 flex items-center justify-center\">\n      <TaskParticles active={filled} distance={dist} accent={accent} />\n\n      <motion.div\n        className=\"absolute inset-0 rounded-full blur-xs pointer-events-none\"\n        style={{ backgroundColor: accent }}\n        initial={false}\n        animate={\n          filled\n            ? { scale: [0.8, 1.5, 1.7], opacity: [0, 0.45, 0] }\n            : { scale: 0.8, opacity: 0 }\n        }\n        transition={\n          filled ? timing({ duration: 0.4, ease: EASE_OUT }) : INSTANT\n        }\n      />\n\n      <motion.svg\n        viewBox=\"0 0 24 24\"\n        aria-hidden\n        className={cn(\n          \"shrink-0 text-zinc-400 dark:text-zinc-500 transition-colors relative z-10\",\n          SIZES[size].check,\n        )}\n        initial={false}\n        animate={{ scale: filled ? POP_SCALE : 1 }}\n        transition={filled ? timing(POP) : INSTANT}\n      >\n        <motion.circle\n          cx=\"12\"\n          cy=\"12\"\n          r={RING_R}\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth=\"1.8\"\n          strokeLinecap=\"round\"\n          strokeDasharray={RING_DASH}\n          initial={false}\n          animate={{ opacity: filled ? 0 : 1 }}\n          transition={timing(FILL)}\n        />\n        <motion.circle\n          cx=\"12\"\n          cy=\"12\"\n          r=\"11.5\"\n          fill={accent}\n          style={{ transformBox: \"view-box\", transformOrigin: \"12px 12px\" }}\n          initial={false}\n          animate={{ scale: filled ? 1 : 0 }}\n          transition={timing(FILL)}\n        />\n        <motion.path\n          d=\"M7.2 12.2 10.5 15.5 16.8 8.7\"\n          fill=\"none\"\n          stroke=\"white\"\n          strokeWidth=\"2.4\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          initial={false}\n          animate={{ pathLength: filled ? 1 : 0, opacity: filled ? 1 : 0 }}\n          transition={timing(TICK)}\n          onAnimationComplete={onDrawn}\n        />\n      </motion.svg>\n    </div>\n  );\n}\n\nfunction TaskLabel({\n  label,\n  struck,\n  size,\n  onStruck,\n}: {\n  label: string;\n  struck: boolean;\n  size: TaskSize;\n  onStruck: () => void;\n}) {\n  const timing = useTiming();\n  const { text, line } = SIZES[size];\n\n  return (\n    <span className=\"min-w-0 flex-1\">\n      <motion.span\n        style={STRIKE_STYLE}\n        className={cn(\n          \"font-medium tracking-[-0.01em] transition-all duration-300 block select-none\",\n          text,\n          struck\n            ? \"text-zinc-400 opacity-60 dark:text-zinc-500\"\n            : \"text-zinc-900 opacity-100 dark:text-zinc-100\",\n        )}\n        initial={false}\n        animate={{\n          backgroundSize: `${struck ? 100 : 0}% ${line}`,\n          filter: struck ? \"blur(0.15px)\" : \"blur(0px)\",\n        }}\n        transition={timing(STRIKE)}\n        onAnimationComplete={onStruck}\n      >\n        {label}\n      </motion.span>\n    </span>\n  );\n}\n\nexport type Task = {\n  id: string;\n  label: string;\n  done?: boolean;\n};\n\nexport type TaskItemProps = Omit<\n  ComponentProps<\"button\">,\n  \"onAnimationStart\" | \"onDrag\" | \"onDragStart\" | \"onDragEnd\"\n> & {\n  label: string;\n  checked?: boolean;\n  defaultChecked?: boolean;\n  size?: TaskSize;\n  accent?: string;\n  onCheckedChange?: (checked: boolean) => void;\n  onSettled?: () => void;\n  onReverted?: () => void;\n};\n\nexport function TaskItem({\n  label,\n  checked,\n  defaultChecked = false,\n  size = \"md\",\n  accent = \"#FF5F2E\",\n  onCheckedChange,\n  onSettled,\n  onReverted,\n  className,\n  onClick,\n  style,\n  ...props\n}: TaskItemProps) {\n  const timing = useTiming();\n  const [own, setOwn] = useState(defaultChecked);\n  const done = checked ?? own;\n\n  const [stage, setStage] = useState<Stage>(done ? STAGE.settled : STAGE.idle);\n  const [was, setWas] = useState(done);\n\n  if (was !== done) {\n    setWas(done);\n    setStage(done ? STAGE.tick : STAGE.unstrike);\n  }\n\n  const onDrawn = () => {\n    if (stage === STAGE.tick) setStage(STAGE.strike);\n    if (stage === STAGE.untick) {\n      setStage(STAGE.idle);\n      onReverted?.();\n    }\n  };\n\n  const onStruck = () => {\n    if (stage === STAGE.strike) setStage(STAGE.nudge);\n    if (stage === STAGE.unstrike) setStage(STAGE.untick);\n  };\n\n  const onFlicked = () => {\n    if (stage !== STAGE.nudge) return;\n    setStage(STAGE.settled);\n    onSettled?.();\n  };\n\n  return (\n    <motion.button\n      type=\"button\"\n      role=\"checkbox\"\n      aria-checked={done}\n      data-slot=\"task-item\"\n      data-state={done ? \"checked\" : \"unchecked\"}\n      style={style}\n      onClick={(event) => {\n        onClick?.(event);\n        if (checked === undefined) setOwn(!done);\n        onCheckedChange?.(!done);\n      }}\n      animate={{ x: stage === STAGE.nudge ? FLICK : 0 }}\n      transition={stage === STAGE.nudge ? timing(NUDGE) : INSTANT}\n      onAnimationComplete={onFlicked}\n      className={cn(\n        \"flex w-full cursor-pointer items-center text-left transition-[border-color,background-color,filter,box-shadow] duration-200\",\n        SIZES[size].row,\n        CARD,\n        FOCUS,\n        done &&\n          \"bg-zinc-100/60 border-zinc-200/40 dark:bg-zinc-950/40 dark:border-white/4\",\n        className,\n      )}\n      {...props}\n    >\n      <TaskCheck\n        filled={FILLED.includes(stage)}\n        size={size}\n        onDrawn={onDrawn}\n        accent={accent}\n      />\n      <TaskLabel\n        label={label}\n        struck={STRUCK.includes(stage)}\n        size={size}\n        onStruck={onStruck}\n      />\n    </motion.button>\n  );\n}\n\nexport type TaskListProps = ComponentProps<\"ul\"> & {\n  tasks?: Task[];\n  defaultTasks?: Task[];\n  size?: TaskSize;\n  accent?: string;\n  reorderCompleted?: boolean;\n  onTasksChange?: (tasks: Task[]) => void;\n};\n\nconst DEFAULT_SAMPLE_TASKS: Task[] = [\n  { id: \"1\", label: \"Refactor animation spring curves\", done: false },\n  { id: \"2\", label: \"Implement micro-particle spark bursts\", done: true },\n  { id: \"3\", label: \"Sub-pixel calibrated hairline strokes\", done: false },\n  { id: \"4\", label: \"Hardware accelerated scaleX transitions\", done: false },\n  { id: \"5\", label: \"Synchronize theme token variables\", done: true },\n];\n\nexport function TaskList({\n  tasks,\n  defaultTasks = DEFAULT_SAMPLE_TASKS,\n  size = \"md\",\n  accent = \"#FF5F2E\",\n  reorderCompleted = true,\n  onTasksChange,\n  className,\n  ...props\n}: TaskListProps) {\n  const timing = useTiming();\n  const [own, setOwn] = useState<Task[]>(defaultTasks);\n  const current = tasks ?? own;\n\n  const [parked, setParked] = useState<string[]>(() =>\n    (tasks ?? defaultTasks).filter((task) => task.done).map((task) => task.id),\n  );\n  const [announcement, setAnnouncement] = useState(\"\");\n\n  const updateTasks = (next: Task[]) => {\n    if (tasks === undefined) setOwn(next);\n    onTasksChange?.(next);\n  };\n\n  const toggle = (task: Task, done: boolean) => {\n    const next = current.map((item) =>\n      item.id === task.id ? { ...item, done } : item,\n    );\n    updateTasks(next);\n    setAnnouncement(`${task.label} ${done ? \"completed\" : \"reopened\"}`);\n  };\n\n  const finished = reorderCompleted\n    ? parked\n        .map((id) => current.find((task) => task.id === id))\n        .filter((task): task is Task => task?.done === true)\n    : [];\n\n  const open = reorderCompleted\n    ? current.filter((task) => !finished.includes(task))\n    : current;\n\n  return (\n    <ul\n      data-slot=\"task-list\"\n      className={cn(\n        \"flex w-full max-w-md flex-col select-none\",\n        SIZES[size].list,\n        className,\n      )}\n      {...props}\n    >\n      {[...open, ...finished].map((task) => (\n        <motion.li\n          key={task.id}\n          layout\n          transition={timing(REORDER)}\n          className=\"w-full list-none\"\n        >\n          <TaskItem\n            label={task.label}\n            checked={!!task.done}\n            size={size}\n            accent={accent}\n            onCheckedChange={(done) => toggle(task, done)}\n            onSettled={() => {\n              if (reorderCompleted) {\n                setParked((ids) =>\n                  ids.includes(task.id) ? ids : [...ids, task.id],\n                );\n              }\n            }}\n            onReverted={() => {\n              if (reorderCompleted) {\n                setParked((ids) => ids.filter((id) => id !== task.id));\n              }\n            }}\n          />\n        </motion.li>\n      ))}\n      <li role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n        {announcement}\n      </li>\n    </ul>\n  );\n}\n\nexport default TaskList;\n",
      "content": "\"use client\";\n\nimport {\n  useState,\n  type ComponentProps,\n  type CSSProperties,\n} from \"react\";\nimport { motion, useReducedMotion, type Transition } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\nconst EASE_OUT = [0.22, 1, 0.36, 1] as const;\nconst EASE_IN_OUT = [0.65, 0, 0.35, 1] as const;\n\nconst POP_SCALE = [1, 1.12, 1];\nconst FLICK = [0, 6, -2, 0];\nconst FLICK_TIMES = [0, 0.35, 0.7, 1];\n\nconst FILL: Transition = { duration: 0.22, ease: EASE_OUT };\nconst POP: Transition = { duration: 0.32, ease: EASE_OUT, times: [0, 0.4, 1] };\nconst TICK: Transition = { duration: 0.24, ease: EASE_OUT, delay: 0.04 };\nconst STRIKE: Transition = { duration: 0.36, ease: EASE_IN_OUT };\nconst NUDGE: Transition = {\n  duration: 0.28,\n  ease: EASE_OUT,\n  times: FLICK_TIMES,\n};\nconst REORDER: Transition = { type: \"spring\", stiffness: 340, damping: 28 };\nconst INSTANT: Transition = { duration: 0 };\n\nconst RING_R = 10.5;\nconst RING_DASH = \"1 4.5\";\n\nconst STRIKE_STYLE: CSSProperties = {\n  backgroundImage: \"linear-gradient(currentColor, currentColor)\",\n  backgroundRepeat: \"no-repeat\",\n  backgroundPosition: \"0 52%\",\n  boxDecorationBreak: \"clone\",\n  WebkitBoxDecorationBreak: \"clone\",\n};\n\nconst SIZES = {\n  sm: {\n    row: \"gap-2.5 rounded-xl px-3 py-2\",\n    check: \"h-4.5 w-4.5\",\n    text: \"text-[13px] leading-5\",\n    line: \"1.5px\",\n    list: \"gap-1.5\",\n    particleDist: 14,\n  },\n  md: {\n    row: \"gap-3 rounded-[14px] px-3.5 py-2.5\",\n    check: \"h-5.5 w-5.5\",\n    text: \"text-[14.5px] leading-6\",\n    line: \"2px\",\n    list: \"gap-2\",\n    particleDist: 18,\n  },\n  lg: {\n    row: \"gap-3.5 rounded-2xl px-4 py-3\",\n    check: \"h-6.5 w-6.5\",\n    text: \"text-[16px] leading-7\",\n    line: \"2.5px\",\n    list: \"gap-2.5\",\n    particleDist: 22,\n  },\n} as const;\n\nexport type TaskSize = keyof typeof SIZES;\n\nconst STAGE = {\n  idle: \"idle\",\n  tick: \"tick\",\n  strike: \"strike\",\n  nudge: \"nudge\",\n  settled: \"settled\",\n  unstrike: \"unstrike\",\n  untick: \"untick\",\n} as const;\ntype Stage = (typeof STAGE)[keyof typeof STAGE];\n\nconst FILLED: Stage[] = [\"tick\", \"strike\", \"nudge\", \"settled\", \"unstrike\"];\nconst STRUCK: Stage[] = [\"strike\", \"nudge\", \"settled\"];\n\nconst CARD =\n  \"bg-white/80 border border-zinc-200/80 shadow-[0_2px_8px_rgba(0,0,0,0.06)] hover:border-zinc-300 hover:bg-zinc-50 active:scale-[0.985] text-zinc-900 backdrop-blur-md dark:bg-zinc-900/70 dark:border-white/8 dark:shadow-[0_2px_8px_rgba(0,0,0,0.25)] dark:hover:border-white/15 dark:hover:bg-zinc-900/90 dark:text-zinc-100\";\nconst FOCUS =\n  \"outline-none focus-visible:ring-2 focus-visible:ring-zinc-400 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-white/40 dark:focus-visible:ring-offset-black\";\n\nconst PARTICLE_ANGLES = [0, 45, 90, 135, 180, 225, 270, 315] as const;\n\nfunction useTiming() {\n  const reduced = useReducedMotion() ?? false;\n  return (transition: Transition) => (reduced ? INSTANT : transition);\n}\n\nfunction TaskParticles({\n  active,\n  distance,\n  accent,\n}: {\n  active: boolean;\n  distance: number;\n  accent: string;\n}) {\n  const timing = useTiming();\n\n  return (\n    <div className=\"absolute inset-0 pointer-events-none flex items-center justify-center\">\n      {PARTICLE_ANGLES.map((angle, i) => {\n        const rad = (angle * Math.PI) / 180;\n        const targetX = Math.cos(rad) * distance;\n        const targetY = Math.sin(rad) * distance;\n        const size = i % 2 === 0 ? 3.5 : 2.5;\n\n        return (\n          <motion.span\n            key={angle}\n            className=\"absolute rounded-full\"\n            style={{\n              width: size,\n              height: size,\n              backgroundColor: accent,\n            }}\n            initial={{ x: 0, y: 0, scale: 0, opacity: 0 }}\n            animate={\n              active\n                ? {\n                    x: [0, targetX * 1.1, targetX],\n                    y: [0, targetY * 1.1, targetY],\n                    scale: [0, 1.4, 0],\n                    opacity: [1, 1, 0],\n                  }\n                : { x: 0, y: 0, scale: 0, opacity: 0 }\n            }\n            transition={\n              active\n                ? timing({\n                    duration: 0.48,\n                    ease: EASE_OUT,\n                    times: [0, 0.4, 1],\n                  })\n                : INSTANT\n            }\n          />\n        );\n      })}\n    </div>\n  );\n}\n\nfunction TaskCheck({\n  filled,\n  size,\n  onDrawn,\n  accent,\n}: {\n  filled: boolean;\n  size: TaskSize;\n  onDrawn: () => void;\n  accent: string;\n}) {\n  const timing = useTiming();\n  const dist = SIZES[size].particleDist;\n\n  return (\n    <div className=\"relative shrink-0 flex items-center justify-center\">\n      <TaskParticles active={filled} distance={dist} accent={accent} />\n\n      <motion.div\n        className=\"absolute inset-0 rounded-full blur-xs pointer-events-none\"\n        style={{ backgroundColor: accent }}\n        initial={false}\n        animate={\n          filled\n            ? { scale: [0.8, 1.5, 1.7], opacity: [0, 0.45, 0] }\n            : { scale: 0.8, opacity: 0 }\n        }\n        transition={\n          filled ? timing({ duration: 0.4, ease: EASE_OUT }) : INSTANT\n        }\n      />\n\n      <motion.svg\n        viewBox=\"0 0 24 24\"\n        aria-hidden\n        className={cn(\n          \"shrink-0 text-zinc-400 dark:text-zinc-500 transition-colors relative z-10\",\n          SIZES[size].check,\n        )}\n        initial={false}\n        animate={{ scale: filled ? POP_SCALE : 1 }}\n        transition={filled ? timing(POP) : INSTANT}\n      >\n        <motion.circle\n          cx=\"12\"\n          cy=\"12\"\n          r={RING_R}\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth=\"1.8\"\n          strokeLinecap=\"round\"\n          strokeDasharray={RING_DASH}\n          initial={false}\n          animate={{ opacity: filled ? 0 : 1 }}\n          transition={timing(FILL)}\n        />\n        <motion.circle\n          cx=\"12\"\n          cy=\"12\"\n          r=\"11.5\"\n          fill={accent}\n          style={{ transformBox: \"view-box\", transformOrigin: \"12px 12px\" }}\n          initial={false}\n          animate={{ scale: filled ? 1 : 0 }}\n          transition={timing(FILL)}\n        />\n        <motion.path\n          d=\"M7.2 12.2 10.5 15.5 16.8 8.7\"\n          fill=\"none\"\n          stroke=\"white\"\n          strokeWidth=\"2.4\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          initial={false}\n          animate={{ pathLength: filled ? 1 : 0, opacity: filled ? 1 : 0 }}\n          transition={timing(TICK)}\n          onAnimationComplete={onDrawn}\n        />\n      </motion.svg>\n    </div>\n  );\n}\n\nfunction TaskLabel({\n  label,\n  struck,\n  size,\n  onStruck,\n}: {\n  label: string;\n  struck: boolean;\n  size: TaskSize;\n  onStruck: () => void;\n}) {\n  const timing = useTiming();\n  const { text, line } = SIZES[size];\n\n  return (\n    <span className=\"min-w-0 flex-1\">\n      <motion.span\n        style={STRIKE_STYLE}\n        className={cn(\n          \"font-medium tracking-[-0.01em] transition-all duration-300 block select-none\",\n          text,\n          struck\n            ? \"text-zinc-400 opacity-60 dark:text-zinc-500\"\n            : \"text-zinc-900 opacity-100 dark:text-zinc-100\",\n        )}\n        initial={false}\n        animate={{\n          backgroundSize: `${struck ? 100 : 0}% ${line}`,\n          filter: struck ? \"blur(0.15px)\" : \"blur(0px)\",\n        }}\n        transition={timing(STRIKE)}\n        onAnimationComplete={onStruck}\n      >\n        {label}\n      </motion.span>\n    </span>\n  );\n}\n\nexport type Task = {\n  id: string;\n  label: string;\n  done?: boolean;\n};\n\nexport type TaskItemProps = Omit<\n  ComponentProps<\"button\">,\n  \"onAnimationStart\" | \"onDrag\" | \"onDragStart\" | \"onDragEnd\"\n> & {\n  label: string;\n  checked?: boolean;\n  defaultChecked?: boolean;\n  size?: TaskSize;\n  accent?: string;\n  onCheckedChange?: (checked: boolean) => void;\n  onSettled?: () => void;\n  onReverted?: () => void;\n};\n\nexport function TaskItem({\n  label,\n  checked,\n  defaultChecked = false,\n  size = \"md\",\n  accent = \"#FF5F2E\",\n  onCheckedChange,\n  onSettled,\n  onReverted,\n  className,\n  onClick,\n  style,\n  ...props\n}: TaskItemProps) {\n  const timing = useTiming();\n  const [own, setOwn] = useState(defaultChecked);\n  const done = checked ?? own;\n\n  const [stage, setStage] = useState<Stage>(done ? STAGE.settled : STAGE.idle);\n  const [was, setWas] = useState(done);\n\n  if (was !== done) {\n    setWas(done);\n    setStage(done ? STAGE.tick : STAGE.unstrike);\n  }\n\n  const onDrawn = () => {\n    if (stage === STAGE.tick) setStage(STAGE.strike);\n    if (stage === STAGE.untick) {\n      setStage(STAGE.idle);\n      onReverted?.();\n    }\n  };\n\n  const onStruck = () => {\n    if (stage === STAGE.strike) setStage(STAGE.nudge);\n    if (stage === STAGE.unstrike) setStage(STAGE.untick);\n  };\n\n  const onFlicked = () => {\n    if (stage !== STAGE.nudge) return;\n    setStage(STAGE.settled);\n    onSettled?.();\n  };\n\n  return (\n    <motion.button\n      type=\"button\"\n      role=\"checkbox\"\n      aria-checked={done}\n      data-slot=\"task-item\"\n      data-state={done ? \"checked\" : \"unchecked\"}\n      style={style}\n      onClick={(event) => {\n        onClick?.(event);\n        if (checked === undefined) setOwn(!done);\n        onCheckedChange?.(!done);\n      }}\n      animate={{ x: stage === STAGE.nudge ? FLICK : 0 }}\n      transition={stage === STAGE.nudge ? timing(NUDGE) : INSTANT}\n      onAnimationComplete={onFlicked}\n      className={cn(\n        \"flex w-full cursor-pointer items-center text-left transition-[border-color,background-color,filter,box-shadow] duration-200\",\n        SIZES[size].row,\n        CARD,\n        FOCUS,\n        done &&\n          \"bg-zinc-100/60 border-zinc-200/40 dark:bg-zinc-950/40 dark:border-white/4\",\n        className,\n      )}\n      {...props}\n    >\n      <TaskCheck\n        filled={FILLED.includes(stage)}\n        size={size}\n        onDrawn={onDrawn}\n        accent={accent}\n      />\n      <TaskLabel\n        label={label}\n        struck={STRUCK.includes(stage)}\n        size={size}\n        onStruck={onStruck}\n      />\n    </motion.button>\n  );\n}\n\nexport type TaskListProps = ComponentProps<\"ul\"> & {\n  tasks?: Task[];\n  defaultTasks?: Task[];\n  size?: TaskSize;\n  accent?: string;\n  reorderCompleted?: boolean;\n  onTasksChange?: (tasks: Task[]) => void;\n};\n\nconst DEFAULT_SAMPLE_TASKS: Task[] = [\n  { id: \"1\", label: \"Refactor animation spring curves\", done: false },\n  { id: \"2\", label: \"Implement micro-particle spark bursts\", done: true },\n  { id: \"3\", label: \"Sub-pixel calibrated hairline strokes\", done: false },\n  { id: \"4\", label: \"Hardware accelerated scaleX transitions\", done: false },\n  { id: \"5\", label: \"Synchronize theme token variables\", done: true },\n];\n\nexport function TaskList({\n  tasks,\n  defaultTasks = DEFAULT_SAMPLE_TASKS,\n  size = \"md\",\n  accent = \"#FF5F2E\",\n  reorderCompleted = true,\n  onTasksChange,\n  className,\n  ...props\n}: TaskListProps) {\n  const timing = useTiming();\n  const [own, setOwn] = useState<Task[]>(defaultTasks);\n  const current = tasks ?? own;\n\n  const [parked, setParked] = useState<string[]>(() =>\n    (tasks ?? defaultTasks).filter((task) => task.done).map((task) => task.id),\n  );\n  const [announcement, setAnnouncement] = useState(\"\");\n\n  const updateTasks = (next: Task[]) => {\n    if (tasks === undefined) setOwn(next);\n    onTasksChange?.(next);\n  };\n\n  const toggle = (task: Task, done: boolean) => {\n    const next = current.map((item) =>\n      item.id === task.id ? { ...item, done } : item,\n    );\n    updateTasks(next);\n    setAnnouncement(`${task.label} ${done ? \"completed\" : \"reopened\"}`);\n  };\n\n  const finished = reorderCompleted\n    ? parked\n        .map((id) => current.find((task) => task.id === id))\n        .filter((task): task is Task => task?.done === true)\n    : [];\n\n  const open = reorderCompleted\n    ? current.filter((task) => !finished.includes(task))\n    : current;\n\n  return (\n    <ul\n      data-slot=\"task-list\"\n      className={cn(\n        \"flex w-full max-w-md flex-col select-none\",\n        SIZES[size].list,\n        className,\n      )}\n      {...props}\n    >\n      {[...open, ...finished].map((task) => (\n        <motion.li\n          key={task.id}\n          layout\n          transition={timing(REORDER)}\n          className=\"w-full list-none\"\n        >\n          <TaskItem\n            label={task.label}\n            checked={!!task.done}\n            size={size}\n            accent={accent}\n            onCheckedChange={(done) => toggle(task, done)}\n            onSettled={() => {\n              if (reorderCompleted) {\n                setParked((ids) =>\n                  ids.includes(task.id) ? ids : [...ids, task.id],\n                );\n              }\n            }}\n            onReverted={() => {\n              if (reorderCompleted) {\n                setParked((ids) => ids.filter((id) => id !== task.id));\n              }\n            }}\n          />\n        </motion.li>\n      ))}\n      <li role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n        {announcement}\n      </li>\n    </ul>\n  );\n}\n\nexport default TaskList;\n",
      "type": "registry:ui",
      "target": "@ui/task-list.tsx"
    }
  ],
  "type": "registry:ui",
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "title": "Task List"
}
