{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "check-list",
  "title": "Check List",
  "description": "A handwritten checklist that writes itself out in full, then ticks and strikes through every done item, one at a time.",
  "dependencies": ["remotion"],
  "registryDependencies": ["@remocn/stop-motion", "@remocn/handwrite"],
  "files": [
    {
      "path": "registry/remocn/check-list/index.tsx",
      "content": "\"use client\";\n\nimport { useCurrentFrame } from \"remotion\";\nimport { Handwrite, handwriteDuration } from \"@/components/remocn/handwrite\";\nimport { DEFAULT_STEP, hashRange, steppedRamp } from \"@/lib/remocn/stop-motion\";\n\nconst easeOutCubic = (t: number) => 1 - (1 - t) ** 3;\n\nconst DEFAULT_PER_STEP = 1.6;\n\nexport type CheckListItem = {\n  text: string;\n  checked?: boolean;\n};\n\nexport type CheckListEntry = {\n  text: string;\n  checked: boolean;\n};\n\nexport type CheckListGeometry = {\n  boxSize: number;\n  boxGap: number;\n  rowHeight: number;\n  labelWidth: number;\n};\n\nexport type CheckListBeat = {\n  boxFrom: number;\n  boxTo: number;\n  labelAt: number;\n  labelEnd: number;\n  tickFrom?: number;\n  tickTo?: number;\n  strikeFrom?: number;\n  strikeTo?: number;\n};\n\nexport type CheckListTiming = {\n  delay?: number;\n  itemGap?: number;\n  perStep?: number;\n  step?: number;\n  closeGap?: number;\n};\n\nexport function normalizeCheckListItems(\n  items: (string | CheckListItem)[],\n): CheckListEntry[] {\n  return items.map((item) =>\n    typeof item === \"string\"\n      ? { text: item, checked: true }\n      : { text: item.text, checked: item.checked ?? true },\n  );\n}\n\nexport function checkListGeometry(\n  width: number,\n  fontSize: number,\n): CheckListGeometry {\n  const boxSize = Math.round(fontSize * 0.62);\n  const boxGap = Math.round(fontSize * 0.4);\n  return {\n    boxSize,\n    boxGap,\n    rowHeight: Math.round(fontSize * 1.15),\n    labelWidth: Math.max(fontSize * 2, width - boxSize - boxGap),\n  };\n}\n\nexport function checkListRowWidth(width: number, fontSize: number): number {\n  const { boxSize, boxGap, labelWidth } = checkListGeometry(width, fontSize);\n  return boxSize + boxGap + labelWidth;\n}\n\nexport function checkListEnterEnd(\n  items: (string | CheckListItem)[],\n  options?: CheckListTiming,\n): number {\n  const step = options?.step ?? DEFAULT_STEP;\n  const delay = options?.delay ?? 0;\n  const itemGap = options?.itemGap ?? step * 6;\n  const perStep = options?.perStep ?? DEFAULT_PER_STEP;\n\n  return normalizeCheckListItems(items).reduce(\n    (end, item, i) =>\n      Math.max(\n        end,\n        delay +\n          i * itemGap +\n          step * 2 +\n          handwriteDuration(item.text, { perStep, step }),\n      ),\n    delay,\n  );\n}\n\nexport function checkListSchedule(\n  items: (string | CheckListItem)[],\n  options?: CheckListTiming,\n): CheckListBeat[] {\n  const step = options?.step ?? DEFAULT_STEP;\n  const delay = options?.delay ?? 0;\n  const itemGap = options?.itemGap ?? step * 6;\n  const perStep = options?.perStep ?? DEFAULT_PER_STEP;\n  const closeGap = options?.closeGap ?? step * 3;\n  const entries = normalizeCheckListItems(items);\n  const enterEnd = checkListEnterEnd(entries, options);\n\n  const closeOrder = new Map<number, number>();\n  entries.forEach((item, i) => {\n    if (item.checked) {\n      closeOrder.set(i, closeOrder.size);\n    }\n  });\n\n  return entries.map((item, i) => {\n    const boxFrom = delay + i * itemGap;\n    const boxTo = boxFrom + step * 2;\n    const labelEnd = boxTo + handwriteDuration(item.text, { perStep, step });\n    const slot = closeOrder.get(i);\n    if (slot === undefined) {\n      return { boxFrom, boxTo, labelAt: boxTo, labelEnd };\n    }\n    const tickFrom = enterEnd + slot * closeGap;\n    const tickTo = tickFrom + step * 2;\n    return {\n      boxFrom,\n      boxTo,\n      labelAt: boxTo,\n      labelEnd,\n      tickFrom,\n      tickTo,\n      strikeFrom: tickTo,\n      strikeTo: tickTo + step * 2,\n    };\n  });\n}\n\nexport function checkListDuration(\n  items: (string | CheckListItem)[],\n  options?: CheckListTiming,\n): number {\n  return checkListSchedule(items, options).reduce(\n    (end, beat) => Math.max(end, beat.strikeTo ?? beat.labelEnd),\n    0,\n  );\n}\n\nconst boxPath = (size: number, seed: string, index: number): string => {\n  const nudge = (n: number) => hashRange(`${seed}:${index}:c${n}`, -1.5, 1.5);\n  const lo = size * 0.08;\n  const hi = size - lo;\n  const corners: [number, number][] = [\n    [lo + nudge(0), lo + nudge(1)],\n    [hi + nudge(2), lo + nudge(3)],\n    [hi + nudge(4), hi + nudge(5)],\n    [lo + nudge(6), hi + nudge(7)],\n  ];\n  const [start, ...rest] = corners;\n  return `M ${start[0]} ${start[1]} ${rest\n    .map(([x, y]) => `L ${x} ${y}`)\n    .join(\" \")} Z`;\n};\n\nconst tickPath = (size: number): string =>\n  `M ${size * 0.2} ${size * 0.52} L ${size * 0.42} ${size * 0.78} L ${size * 0.88} ${size * 0.14}`;\n\nconst CAVEAT_ADVANCE = 0.36;\n\nexport function checkListStrikeWidth(\n  text: string,\n  fontSize: number,\n  labelWidth: number,\n): number {\n  return Math.min(\n    labelWidth,\n    Array.from(text).length * fontSize * CAVEAT_ADVANCE,\n  );\n}\n\nconst strikePath = (\n  reach: number,\n  fontSize: number,\n  rowHeight: number,\n  seed: string,\n  index: number,\n): string => {\n  const lift = (n: number) => hashRange(`${seed}:${index}:s${n}`, -1.6, 1.6);\n  const overshoot = fontSize * 0.07;\n  const mid = rowHeight / 2;\n  return `M ${-overshoot} ${mid + lift(0)} C ${reach * 0.35} ${mid + lift(1)}, ${reach * 0.7} ${mid + lift(2)}, ${reach + overshoot} ${mid + lift(3)}`;\n};\n\nexport interface CheckListProps {\n  items: (string | CheckListItem)[];\n  width: number;\n  fontSize?: number;\n  color?: string;\n  boxColor?: string;\n  tickColor?: string;\n  delay?: number;\n  itemGap?: number;\n  closeGap?: number;\n  rowGap?: number;\n  strokeWidth?: number;\n  perStep?: number;\n  weight?: 400 | 500 | 600 | 700;\n  seed?: string;\n  step?: number;\n}\n\nexport function CheckList({\n  items,\n  width,\n  fontSize = 40,\n  color = \"#26242c\",\n  boxColor = \"#26242c\",\n  tickColor = \"#6f7f35\",\n  delay = 0,\n  itemGap,\n  closeGap,\n  rowGap,\n  strokeWidth = 3,\n  perStep = DEFAULT_PER_STEP,\n  weight = 600,\n  seed = \"checklist\",\n  step = DEFAULT_STEP,\n}: CheckListProps) {\n  const frame = useCurrentFrame();\n  const entries = normalizeCheckListItems(items);\n  const schedule = checkListSchedule(entries, {\n    delay,\n    itemGap,\n    closeGap,\n    perStep,\n    step,\n  });\n  const { boxSize, boxGap, rowHeight, labelWidth } = checkListGeometry(\n    width,\n    fontSize,\n  );\n\n  return (\n    <div\n      style={{\n        width: checkListRowWidth(width, fontSize),\n        display: \"flex\",\n        flexDirection: \"column\",\n        gap: rowGap ?? fontSize * 0.55,\n      }}\n    >\n      {entries.map((item, i) => {\n        const beat = schedule[i];\n        const boxProgress = steppedRamp(frame, beat.boxFrom, beat.boxTo, {\n          ease: easeOutCubic,\n          step,\n        });\n        const tickProgress =\n          beat.tickFrom === undefined || beat.tickTo === undefined\n            ? 0\n            : steppedRamp(frame, beat.tickFrom, beat.tickTo, {\n                ease: easeOutCubic,\n                step,\n              });\n        const strikeProgress =\n          beat.strikeFrom === undefined || beat.strikeTo === undefined\n            ? 0\n            : steppedRamp(frame, beat.strikeFrom, beat.strikeTo, {\n                ease: easeOutCubic,\n                step,\n              });\n\n        return (\n          <div\n            key={`${item.text}:${i}`}\n            style={{\n              display: \"flex\",\n              alignItems: \"center\",\n              gap: boxGap,\n              height: rowHeight,\n            }}\n          >\n            <svg\n              width={boxSize}\n              height={boxSize}\n              viewBox={`0 0 ${boxSize} ${boxSize}`}\n              style={{ display: \"block\", overflow: \"visible\", flexShrink: 0 }}\n            >\n              <path\n                d={boxPath(boxSize, seed, i)}\n                stroke={boxColor}\n                strokeWidth={strokeWidth}\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n                fill=\"none\"\n                pathLength={1}\n                strokeDasharray={1}\n                strokeDashoffset={1 - boxProgress}\n                opacity={boxProgress > 0 ? 0.9 : 0}\n              />\n              {item.checked ? (\n                <path\n                  d={tickPath(boxSize)}\n                  stroke={tickColor}\n                  strokeWidth={strokeWidth + 1}\n                  strokeLinecap=\"round\"\n                  strokeLinejoin=\"round\"\n                  fill=\"none\"\n                  pathLength={1}\n                  strokeDasharray={1}\n                  strokeDashoffset={1 - tickProgress}\n                  opacity={tickProgress > 0 ? 1 : 0}\n                />\n              ) : null}\n            </svg>\n            <div\n              style={{\n                position: \"relative\",\n                flexShrink: 0,\n                width: labelWidth,\n                height: rowHeight,\n              }}\n            >\n              <Handwrite\n                text={item.text}\n                fontSize={fontSize}\n                color={color}\n                delay={beat.labelAt}\n                perStep={perStep}\n                weight={weight}\n                align=\"left\"\n                step={step}\n              />\n              {item.checked ? (\n                <svg\n                  width={labelWidth}\n                  height={rowHeight}\n                  viewBox={`0 0 ${labelWidth} ${rowHeight}`}\n                  style={{\n                    position: \"absolute\",\n                    inset: 0,\n                    display: \"block\",\n                    overflow: \"visible\",\n                  }}\n                >\n                  <path\n                    d={strikePath(\n                      checkListStrikeWidth(item.text, fontSize, labelWidth),\n                      fontSize,\n                      rowHeight,\n                      seed,\n                      i,\n                    )}\n                    stroke={tickColor}\n                    strokeWidth={strokeWidth}\n                    strokeLinecap=\"round\"\n                    fill=\"none\"\n                    pathLength={1}\n                    strokeDasharray={1}\n                    strokeDashoffset={1 - strikeProgress}\n                    opacity={strikeProgress > 0 ? 0.85 : 0}\n                  />\n                </svg>\n              ) : null}\n            </div>\n          </div>\n        );\n      })}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/check-list.tsx"
    }
  ],
  "type": "registry:component"
}
