{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "task-list",
  "type": "registry:ui",
  "title": "Task List",
  "description": "Interactive todo task list featuring micro-particle spark bursts, ambient glow halos, smooth SVG ring morphing, and fluid spring layout reordering.",
  "dependencies": [
    "motion",
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/registry/ui/task-list.tsx",
      "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"
    }
  ]
}
