{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "author": "Lopifi (https://lopifi.com)",
  "categories": [
    "statements",
    "finance",
    "accounting"
  ],
  "dependencies": [],
  "description": "Tasks, owners, blockers, sign-off.",
  "files": [
    {
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/ledger/cn\";\nimport { daysBetween, formatDate } from \"@/lib/ledger/fiscal-period\";\nimport { Button } from \"@/components/ledger/button\";\nimport { HoldToConfirm } from \"@/components/ledger/hold-to-confirm\";\nimport { CheckIcon, ExclamationTriangleIcon, LockClosedIcon } from \"@/components/ledger/icons\";\n\nexport type CloseTask = {\n  id: string;\n  section: string;\n  label: string;\n  owner: string;\n  dueDate: Date;\n  state: \"todo\" | \"in-progress\" | \"done\" | \"blocked\";\n  blockedBy?: string;\n  /** Weighted, because ten trivial checks and one bank rec is not 91% done. */\n  weight: number;\n};\n\nexport interface CloseChecklistProps {\n  className?: string;\n  onSignOff?: () => void;\n  onToggle?: (id: string) => void;\n  tasks: CloseTask[];\n  today?: Date;\n}\n\n/** Progress is by weighted check, not count. Sign-off is irreversible. */\nexport function CloseChecklist({\n  tasks,\n  today = new Date(),\n  onToggle,\n  onSignOff,\n  className,\n}: CloseChecklistProps) {\n  const sections = React.useMemo(() => {\n    const map = new Map<string, CloseTask[]>();\n    for (const t of tasks) {\n      const list = map.get(t.section) ?? [];\n      list.push(t);\n      map.set(t.section, list);\n    }\n    return [...map.entries()];\n  }, [tasks]);\n\n  const totalWeight = tasks.reduce((s, t) => s + t.weight, 0);\n  const doneWeight = tasks.filter((t) => t.state === \"done\").reduce((s, t) => s + t.weight, 0);\n  const percent = totalWeight === 0 ? 0 : Math.round((doneWeight / totalWeight) * 100);\n  const allDone = doneWeight === totalWeight;\n\n  return (\n    <div className={cn(\"space-y-4\", className)}>\n      <div className=\"rounded-md border border-border bg-surface p-4\">\n        <div className=\"flex items-baseline justify-between\">\n          <span className=\"text-label font-medium\">Close progress</span>\n          <span className=\"numeric text-figure-md font-medium\">{percent}%</span>\n        </div>\n        <div\n          aria-label=\"Close progress, weighted\"\n          aria-valuemax={100}\n          aria-valuemin={0}\n          aria-valuenow={percent}\n          className=\"mt-2 h-[3px] w-full bg-border-subtle\"\n          role=\"progressbar\"\n        >\n          <div className=\"h-full bg-accent\" style={{ width: `${percent}%` }} />\n        </div>\n        <p className=\"mt-2 text-caption text-text-tertiary\">Weighted by effort, not by task count.</p>\n      </div>\n\n      {sections.map(([section, items]) => (\n        <section className=\"overflow-hidden rounded-xl border border-border bg-surface\" key={section}>\n          <h3 className=\"border-b border-border-subtle px-4 py-3 text-label font-medium text-text-secondary\">\n            {section}\n          </h3>\n          <ul>\n            {items.map((t) => {\n              const overdueDays = daysBetween(t.dueDate, today);\n              const overdue = overdueDays > 0 && t.state !== \"done\";\n              return (\n                <li\n                  className=\"flex items-center gap-3 border-b border-border-subtle px-4 py-3 last:border-b-0\"\n                  key={t.id}\n                >\n                  <button\n                    aria-label={`Mark ${t.label} ${t.state === \"done\" ? \"not done\" : \"done\"}`}\n                    aria-pressed={t.state === \"done\"}\n                    className=\"grid size-5 shrink-0 place-items-center rounded-full border border-border-strong disabled:opacity-40\"\n                    disabled={t.state === \"blocked\"}\n                    onClick={() => onToggle?.(t.id)}\n                    type=\"button\"\n                  >\n                    {t.state === \"done\" ? (\n                      <CheckIcon aria-hidden=\"true\" className=\"size-3 text-positive\" />\n                    ) : t.state === \"blocked\" ? (\n                      <LockClosedIcon aria-hidden=\"true\" className=\"size-3 text-text-tertiary\" />\n                    ) : (\n                      <span\n                        aria-hidden=\"true\"\n                        className=\"size-2 rounded-full bg-current text-text-disabled\"\n                      />\n                    )}\n                  </button>\n                  <span className=\"min-w-0 flex-1\">\n                    <span\n                      className={cn(\n                        \"block text-body\",\n                        t.state === \"done\" && \"text-text-secondary line-through\",\n                      )}\n                    >\n                      {t.label}\n                    </span>\n                    {t.blockedBy ? (\n                      <span className=\"block text-caption text-warning\">Blocked by: {t.blockedBy}</span>\n                    ) : null}\n                  </span>\n                  <span className=\"shrink-0 text-caption text-text-secondary\">{t.owner}</span>\n                  <span\n                    className={cn(\n                      \"numeric w-28 shrink-0 text-right text-caption\",\n                      overdue ? \"text-negative\" : \"text-text-tertiary\",\n                    )}\n                  >\n                    {overdue ? (\n                      <span className=\"inline-flex items-center gap-1\">\n                        <ExclamationTriangleIcon aria-hidden=\"true\" className=\"size-3\" />\n                        {overdueDays}d overdue\n                      </span>\n                    ) : (\n                      formatDate(t.dueDate)\n                    )}\n                  </span>\n                </li>\n              );\n            })}\n          </ul>\n        </section>\n      ))}\n\n      <div className=\"rounded-md border border-border bg-surface p-4\">\n        <p className=\"mb-3 text-body\">\n          Signing off a close is irreversible. Statements generated after sign-off are marked final.\n        </p>\n        {allDone ? (\n          <HoldToConfirm\n            label=\"Hold to sign off the close\"\n            onConfirm={() => onSignOff?.()}\n            typedConfirmation=\"SIGN OFF\"\n          />\n        ) : (\n          <Button disabled variant=\"primary\">\n            Sign off — {tasks.filter((t) => t.state !== \"done\").length} tasks remain\n          </Button>\n        )}\n      </div>\n    </div>\n  );\n}\n",
      "path": "components/ledger/close-checklist.tsx",
      "target": "components/ledger/close-checklist.tsx",
      "type": "registry:component"
    }
  ],
  "name": "close-checklist",
  "registryDependencies": [
    "@lopifi/theme",
    "@lopifi/button",
    "@lopifi/cn",
    "@lopifi/fiscal-period",
    "@lopifi/hold-to-confirm",
    "@lopifi/icons"
  ],
  "title": "CloseChecklist",
  "type": "registry:component"
}
