{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "checklist-cell",
  "type": "registry:ui",
  "title": "Checklist Cell",
  "description": "Animated checklist illustration with sliding tasks and progressive completion.",
  "author": "Ahdeetai <https://aditya.is-cool.dev>",
  "registryDependencies": [],
  "dependencies": ["motion", "lucide-react", "clsx", "tailwind-merge"],
  "files": [
    {
      "type": "registry:ui",
      "path": "components/ui/checklist-cell.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\nimport { motion, AnimatePresence } from 'motion/react';\nimport { ChevronRight } from 'lucide-react';\nimport { cn } from '@/lib/utils';\n\nconst DEFAULT_TASKS = [\n  'How it all started',\n  'Our values and approach',\n  'Working together',\n  'Union rules and integration',\n  'Team structure',\n  'Communication norms',\n  'Feedback culture',\n  'Onboarding wrap-up',\n];\n\nexport interface ChecklistCellProps extends React.HTMLAttributes<HTMLDivElement> {\n  tasks?: string[];\n  initialCompleted?: number;\n  finalCompleted?: number;\n  stepInterval?: number;\n  hoverToPlay?: boolean;\n}\n\nfunction FilledCheck() {\n  return (\n    <svg\n      width='18'\n      height='18'\n      viewBox='0 0 18 18'\n      fill='none'\n      xmlns='http://www.w3.org/2000/svg'\n      className='shrink-0'\n    >\n      <circle cx='9' cy='9' r='9' fill='#22c55e' />\n      <path\n        d='M5.5 9.5l2.5 2.5 4.5-5'\n        stroke='white'\n        strokeWidth='1.5'\n        strokeLinecap='round'\n        strokeLinejoin='round'\n      />\n    </svg>\n  );\n}\n\nfunction EmptyCircle({ muted }: { muted?: boolean }) {\n  return (\n    <svg\n      width='18'\n      height='18'\n      viewBox='0 0 18 18'\n      fill='none'\n      xmlns='http://www.w3.org/2000/svg'\n      className='shrink-0'\n    >\n      <circle\n        cx='9'\n        cy='9'\n        r='8'\n        stroke={muted ? '#e4e4e7' : '#d4d4d8'}\n        strokeWidth='1.5'\n      />\n    </svg>\n  );\n}\n\nconst FADE_DURATION = 350;\n\nconst ChecklistCell = React.forwardRef<HTMLDivElement, ChecklistCellProps>(\n  (\n    {\n      className,\n      tasks = DEFAULT_TASKS,\n      initialCompleted = 3,\n      finalCompleted,\n      stepInterval = 1700,\n      hoverToPlay = false,\n      ...props\n    },\n    ref,\n  ) => {\n    const total = tasks.length;\n    const maxDone = finalCompleted !== undefined\n      ? Math.min(finalCompleted, total - 1)\n      : total - 1;\n\n    const [active, setActive] = React.useState(!hoverToPlay);\n    const [phase, setPhase] = React.useState<'playing' | 'fading'>('playing');\n    const [done, setDone] = React.useState(initialCompleted);\n    const [winStart, setWinStart] = React.useState(1);\n    const [sliding, setSliding] = React.useState(false);\n    const [checkingSlot, setCheckingSlot] = React.useState(false);\n\n    const pct = (done / total) * 100;\n    const hasNext = winStart + 3 < total;\n\n    const resetState = React.useCallback(() => {\n      setDone(initialCompleted);\n      setWinStart(1);\n      setSliding(false);\n      setCheckingSlot(false);\n    }, [initialCompleted]);\n\n    React.useEffect(() => {\n      if (!active || phase !== 'playing') return;\n      if (done >= maxDone || !hasNext) {\n        const t = setTimeout(() => setPhase('fading'), 2000);\n        return () => clearTimeout(t);\n      }\n\n      const t = setTimeout(() => {\n        setSliding(true);\n\n        const t2 = setTimeout(() => {\n          setWinStart((w) => w + 1);\n          setSliding(false);\n\n          const t3 = setTimeout(() => {\n            setCheckingSlot(true);\n            setDone((d) => d + 1);\n            const t4 = setTimeout(() => setCheckingSlot(false), 500);\n            return () => clearTimeout(t4);\n          }, 80);\n          return () => clearTimeout(t3);\n        }, 700);\n        return () => clearTimeout(t2);\n      }, stepInterval);\n\n      return () => clearTimeout(t);\n    }, [active, phase, done, maxDone, hasNext, stepInterval]);\n\n    React.useEffect(() => {\n      if (phase !== 'fading') return;\n\n      const tReset = setTimeout(resetState, FADE_DURATION + 50);\n      const tPlay = setTimeout(() => setPhase('playing'), FADE_DURATION + 150);\n\n      return () => {\n        clearTimeout(tReset);\n        clearTimeout(tPlay);\n      };\n    }, [phase, resetState]);\n\n    const handleMouseEnter = React.useCallback(() => {\n      if (hoverToPlay) setActive(true);\n    }, [hoverToPlay]);\n\n    const handleMouseLeave = React.useCallback(() => {\n      if (hoverToPlay) {\n        setActive(false);\n        setPhase('fading');\n      }\n    }, [hoverToPlay]);\n\n    const slots = hasNext\n      ? tasks.slice(winStart, winStart + 4)\n      : tasks.slice(winStart, winStart + 3);\n\n    return (\n      <div\n        ref={ref}\n        className={cn('flex flex-col gap-4 p-4', className)}\n        onMouseEnter={handleMouseEnter}\n        onMouseLeave={handleMouseLeave}\n        {...props}\n      >\n        <div className='flex items-center gap-3'>\n          <div className='h-2 flex-1 overflow-hidden rounded-full bg-zinc-100 dark:bg-zinc-800'>\n            <motion.div\n              className='h-full rounded-full bg-zinc-900 dark:bg-zinc-100'\n              animate={{ width: `${pct}%` }}\n              transition={{ duration: 0.6, ease: [0.4, 0, 0.2, 1] }}\n            />\n          </div>\n\n          <div className='flex items-center whitespace-nowrap text-[11px] text-zinc-400 overflow-hidden'>\n            <div className='relative h-4 overflow-hidden' style={{ minWidth: '7px' }}>\n              <AnimatePresence mode='popLayout' initial={false}>\n                <motion.span\n                  key={done}\n                  initial={{ y: '100%', opacity: 0 }}\n                  animate={{ y: '0%', opacity: 1 }}\n                  exit={{ y: '-100%', opacity: 0 }}\n                  transition={{ duration: 0.25, ease: [0.4, 0, 0.2, 1] }}\n                  className='absolute inset-0 flex items-center justify-end'\n                >\n                  {done}\n                </motion.span>\n              </AnimatePresence>\n            </div>\n            <span className='mx-0.5'>/</span>\n            <span>{total} Completed</span>\n          </div>\n        </div>\n\n        <motion.div\n          className='relative h-33 overflow-hidden'\n          animate={{ opacity: phase === 'fading' ? 0 : 1 }}\n          transition={{ duration: FADE_DURATION / 1000, ease: [0.4, 0, 0.2, 1] }}\n        >\n          <div className='pointer-events-none absolute inset-x-0 bottom-0 z-10 h-12 bg-linear-to-t from-white to-transparent dark:from-zinc-950' />\n\n          <AnimatePresence initial={false}>\n            {slots.map((task, i) => {\n              const globalIdx = winStart + i;\n              const isDone = globalIdx < done;\n              const isMidAndChecking = i === 1 && checkingSlot;\n              const isChecked = isDone || isMidAndChecking;\n              const isIncoming = i === 3;\n\n              return (\n                <motion.div\n                  key={`${globalIdx}`}\n                  initial={isIncoming ? { y: 132 + 20, opacity: 0 } : { y: i * 44, opacity: 1 }}\n                  animate={{\n                    y: sliding ? (i - 1) * 44 : i * 44,\n                    opacity: sliding && i === 0 ? 0 : 1,\n                  }}\n                  exit={{ y: -44, opacity: 0 }}\n                  transition={{ duration: 0.48, ease: [0.4, 0, 0.2, 1] }}\n                  className='absolute left-0 right-0 flex items-center gap-2.5 px-0.5 py-2.5'\n                >\n                  <AnimatePresence mode='wait'>\n                    {isChecked ? (\n                      <motion.div\n                        key='check'\n                        initial={{ scale: 0 }}\n                        animate={{ scale: 1 }}\n                        exit={{ scale: 0 }}\n                        transition={{ type: 'spring', stiffness: 320, damping: 18, mass: 0.8 }}\n                      >\n                        <FilledCheck />\n                      </motion.div>\n                    ) : (\n                      <motion.div key='circle' initial={{ scale: 1 }} animate={{ scale: 1 }}>\n                        <EmptyCircle muted={isIncoming} />\n                      </motion.div>\n                    )}\n                  </AnimatePresence>\n\n                  <span\n                    className={cn(\n                      'text-sm transition-colors duration-300',\n                      isIncoming\n                        ? 'text-zinc-400 dark:text-zinc-600'\n                        : 'text-zinc-800 dark:text-zinc-200',\n                    )}\n                  >\n                    {task}\n                  </span>\n\n                  <ChevronRight className='ml-auto h-3.5 w-3.5 shrink-0 text-zinc-200 dark:text-zinc-700' />\n                </motion.div>\n              );\n            })}\n          </AnimatePresence>\n        </motion.div>\n      </div>\n    );\n  },\n);\n\nChecklistCell.displayName = 'ChecklistCell';\n\nexport { ChecklistCell, DEFAULT_TASKS };\nexport default ChecklistCell;"
    },
    {
      "type": "registry:lib",
      "path": "lib/utils.ts",
      "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}"
    }
  ]
}
