{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "author": "Lopifi (https://lopifi.com)",
  "categories": [
    "accounting-core-gl",
    "finance",
    "accounting"
  ],
  "dependencies": [],
  "description": "Every BS account, proven against its evidence.",
  "files": [
    {
      "content": "import * as React from \"react\";\nimport { useScrollEdges } from \"@/hooks/ledger/use-scroll-edges\";\nimport { cn } from \"@/lib/ledger/cn\";\nimport { formatDate } from \"@/lib/ledger/fiscal-period\";\nimport { FROZEN_LABEL } from \"@/lib/ledger/frozen-label\";\nimport type { MoneyValue } from \"@/lib/ledger/money.types\";\nimport { CheckIcon, DocumentMagnifyingGlassIcon, ExclamationTriangleIcon, PaperClipIcon } from \"@/components/ledger/icons\";\nimport { Money } from \"@/components/ledger/money\";\n\nexport type TieOutRow = {\n  accountNumber: string;\n  accountName: string;\n  glBalance: MoneyValue;\n  /** null means there is no supporting schedule at all. */\n  scheduleBalance: MoneyValue | null;\n  evidence?: { name: string; hash: string };\n  reviewer?: string;\n  reviewedAt?: Date;\n};\n\nexport interface AccountTieOutProps extends React.ComponentPropsWithoutRef<\"div\"> {\n  asOfLabel: string;\n  /**\n   * Caps the scroller so the header and the account column can freeze. Without a\n   * height the page scrolls instead and there is nothing for the frozen\n   * header to stick to.\n   */\n  maxHeight?: number;\n  rows: TieOutRow[];\n}\n\n/**\n * The balance-sheet walk-down. **Every balance sheet account gets one.**\n * An account with no supporting evidence renders an explicit \"Unsupported\"\n * state rather than nothing — nothing reads as fine.\n */\nexport const AccountTieOut = React.forwardRef<HTMLDivElement, AccountTieOutProps>(\n  ({ className, rows, asOfLabel, maxHeight = 520, ...props }, ref) => {\n    const scroller = useScrollEdges<HTMLDivElement>();\n    const unsupported = rows.filter((r) => r.scheduleBalance === null);\n    const breaks = rows.filter(\n      (r) => r.scheduleBalance !== null && Number(r.scheduleBalance.amount) !== Number(r.glBalance.amount),\n    );\n\n    return (\n      <div className={cn(\"space-y-3\", className)} ref={ref} {...props}>\n        <div className=\"flex flex-wrap gap-2\">\n          <Chip\n            label={`${breaks.length} account${breaks.length === 1 ? \"\" : \"s\"} out of tie`}\n            tone={breaks.length === 0 ? \"positive\" : \"negative\"}\n          />\n          <Chip\n            label={`${unsupported.length} unsupported`}\n            tone={unsupported.length === 0 ? \"positive\" : \"warning\"}\n          />\n        </div>\n\n        <div className=\"rounded-xl border border-border bg-surface\">\n          <div className=\"px-6 pb-4 pt-6\">\n            <p className=\"text-title-sm font-medium\">Balance sheet account tie-out</p>\n            <p className=\"text-caption text-text-secondary\">{asOfLabel}</p>\n          </div>\n          <div className=\"ledger-frozen scrollbar-thin\" ref={scroller} style={{ maxHeight }}>\n            <table className=\"w-full min-w-[860px]\">\n              <caption className=\"sr-only\">Balance sheet account tie-out. {asOfLabel}</caption>\n              <thead>\n                <tr className=\"border-b border-border-subtle\">\n                  {[\"Account\", \"GL balance\", \"Schedule\", \"Difference\", \"Evidence\", \"Reviewed\"].map((h, i) => (\n                    <th\n                      className={cn(\n                        \"px-3 py-3 text-label font-medium text-text-secondary\",\n                        i >= 1 && i <= 3 ? \"text-right\" : \"text-left\",\n                        i === 0 && `pl-6 ${FROZEN_LABEL}`,\n                        i === 5 && \"pr-6\",\n                      )}\n                      key={h}\n                      scope=\"col\"\n                    >\n                      {h}\n                    </th>\n                  ))}\n                </tr>\n              </thead>\n              <tbody>\n                {rows.map((r) => {\n                  /* Held as a value rather than a boolean so the null check\n                     narrows the type for every use below it. */\n                  const scheduleBalance = r.scheduleBalance;\n                  const unsupportedRow = scheduleBalance === null;\n                  const difference = scheduleBalance\n                    ? Number(r.glBalance.amount) - Number(scheduleBalance.amount)\n                    : null;\n                  const ties = difference === 0;\n\n                  return (\n                    <tr\n                      className={cn(\n                        \"border-b border-border-subtle\",\n                        !unsupportedRow && !ties && \"bg-negative-subtle\",\n                        unsupportedRow && \"bg-warning-subtle\",\n                      )}\n                      key={r.accountNumber}\n                    >\n                      <th\n                        className=\"sticky left-0 z-sticky bg-[inherit] shadow-[1px_0_0_var(--border-subtle)] py-3 pl-6 pr-3 text-left font-normal\"\n                        scope=\"row\"\n                      >\n                        <span className=\"reference-numeric text-caption text-text-tertiary\">\n                          {r.accountNumber}\n                        </span>{\" \"}\n                        <span className=\"text-body\">{r.accountName}</span>\n                      </th>\n                      <td className=\"px-3 py-3 text-right\">\n                        <Money accounting value={r.glBalance} />\n                      </td>\n                      <td className=\"px-3 py-3 text-right\">\n                        {scheduleBalance === null ? (\n                          <span className=\"inline-flex items-center gap-1 text-caption text-warning\">\n                            <DocumentMagnifyingGlassIcon aria-hidden=\"true\" className=\"size-3.5\" />\n                            Unsupported\n                          </span>\n                        ) : (\n                          <Money accounting value={scheduleBalance} />\n                        )}\n                      </td>\n                      <td className=\"px-3 py-3 text-right\">\n                        {difference === null ? (\n                          <span className=\"numeric relative text-figure-sm text-text-tertiary\">\n                            <span aria-hidden=\"true\">n/a</span>\n                            <span className=\"sr-only select-none\">not available</span>\n                          </span>\n                        ) : (\n                          <span\n                            className={cn(\n                              \"inline-flex items-center gap-1\",\n                              ties ? \"text-positive\" : \"text-negative\",\n                            )}\n                          >\n                            {ties ? (\n                              <CheckIcon aria-hidden=\"true\" className=\"size-3.5\" />\n                            ) : (\n                              <ExclamationTriangleIcon aria-hidden=\"true\" className=\"size-3.5\" />\n                            )}\n                            <Money\n                              accounting\n                              tone=\"inherit\"\n                              value={{ amount: difference, currency: r.glBalance.currency }}\n                            />\n                          </span>\n                        )}\n                      </td>\n                      <td className=\"px-3 py-3 text-left\">\n                        {r.evidence ? (\n                          <span className=\"inline-flex items-center gap-1 text-caption text-text-secondary\">\n                            <PaperClipIcon aria-hidden=\"true\" className=\"size-3\" />\n                            {r.evidence.name}\n                          </span>\n                        ) : (\n                          <span className=\"text-caption text-warning\">None attached</span>\n                        )}\n                      </td>\n                      <td className=\"py-3 pl-3 pr-6 text-left text-caption text-text-secondary\">\n                        {r.reviewer && r.reviewedAt ? (\n                          <>\n                            {r.reviewer}\n                            <span className=\"block text-text-tertiary\">{formatDate(r.reviewedAt)}</span>\n                          </>\n                        ) : (\n                          <span className=\"text-warning\">Not reviewed</span>\n                        )}\n                      </td>\n                    </tr>\n                  );\n                })}\n              </tbody>\n            </table>\n          </div>\n        </div>\n      </div>\n    );\n  },\n);\nAccountTieOut.displayName = \"AccountTieOut\";\n\nfunction Chip({ tone, label }: { tone: \"positive\" | \"negative\" | \"warning\"; label: string }) {\n  return (\n    <span\n      className={cn(\n        \"rounded-sm px-2 py-1 text-caption font-medium\",\n        tone === \"positive\" && \"bg-positive-subtle text-positive\",\n        tone === \"negative\" && \"bg-negative-subtle text-negative\",\n        tone === \"warning\" && \"bg-warning-subtle text-warning\",\n      )}\n    >\n      {label}\n    </span>\n  );\n}\n",
      "path": "components/ledger/account-tie-out.tsx",
      "target": "components/ledger/account-tie-out.tsx",
      "type": "registry:component"
    }
  ],
  "name": "account-tie-out",
  "registryDependencies": [
    "@lopifi/theme",
    "@lopifi/cn",
    "@lopifi/fiscal-period",
    "@lopifi/frozen-label",
    "@lopifi/icons",
    "@lopifi/money",
    "@lopifi/money.types",
    "@lopifi/use-scroll-edges"
  ],
  "title": "AccountTieOut",
  "type": "registry:component"
}
