{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "author": "Lopifi (https://lopifi.com)",
  "categories": [
    "accounting-core-gl",
    "finance",
    "accounting"
  ],
  "dependencies": [],
  "description": "Debit and credit as two columns. No signed-column variant.",
  "files": [
    {
      "content": "import * as React from \"react\";\nimport type { JournalEntryRecord } from \"@/lib/ledger/accounting.types\";\nimport { cn } from \"@/lib/ledger/cn\";\nimport { formatDate } from \"@/lib/ledger/fiscal-period\";\nimport type { MoneyValue } from \"@/lib/ledger/money.types\";\nimport { ArrowUturnLeftIcon, CheckIcon, ExclamationTriangleIcon, PaperClipIcon } from \"@/components/ledger/icons\";\nimport { InfoTip } from \"@/components/ledger/info-tip\";\nimport { Money } from \"@/components/ledger/money\";\nimport { Reference } from \"@/components/ledger/reference\";\nimport { Rule } from \"@/components/ledger/rule\";\nimport { Tooltip } from \"@/components/ledger/tooltip\";\n\nexport interface JournalEntryProps extends React.ComponentPropsWithoutRef<\"article\"> {\n  actions?: React.ReactNode;\n  currency: string;\n  entry: JournalEntryRecord;\n  totalCredit: MoneyValue;\n  /** Totals passed in, already computed. */\n  totalDebit: MoneyValue;\n}\n\nconst SOURCE_LABEL: Record<string, string> = {\n  adjusting: \"Adjusting\",\n  elimination: \"Elimination\",\n  import: \"Import\",\n  manual: \"Manual\",\n  recurring: \"Recurring\",\n  schedule: \"Schedule\",\n  subledger: \"Subledger\",\n  translation: \"Translation\",\n};\n\n/**\n * Debit and credit as two columns. There is no single-signed-column variant.\n *\n * A posted entry is never edited or deleted — it is reversed. The delete\n * action is **absent**, not disabled.\n *\n * Server-safe: a 400-line entry renders on the server.\n */\nexport const JournalEntry = React.forwardRef<HTMLElement, JournalEntryProps>(\n  ({ className, entry, currency, totalDebit, totalCredit, actions, ...props }, ref) => {\n    const balanced = Number(totalDebit.amount) === Number(totalCredit.amount);\n    const difference = Number(totalDebit.amount) - Number(totalCredit.amount);\n\n    return (\n      <article className={cn(\"rounded-xl border border-border bg-surface\", className)} ref={ref} {...props}>\n        <header className=\"flex flex-wrap items-start justify-between gap-4 border-b border-border-subtle px-6 py-4\">\n          <div>\n            <p className=\"flex flex-wrap items-center gap-2 text-caption text-text-secondary\">\n              <Reference truncate={false} value={entry.entryNumber} />\n              <span aria-hidden=\"true\">·</span>\n              <span className=\"numeric\">{formatDate(entry.date)}</span>\n              <span aria-hidden=\"true\">·</span>\n              {SOURCE_LABEL[entry.source] ?? entry.source}\n              <span aria-hidden=\"true\">·</span>\n              <span className=\"capitalize\">{entry.status}</span>\n            </p>\n            <p className=\"mt-1 text-body font-medium\">{entry.description}</p>\n          </div>\n          {/* No delete. Absent, not disabled. */}\n          <div className=\"flex gap-2\">{actions}</div>\n        </header>\n\n        {/* Never sortable: the lines of one entry read debits first, then\n            credits, and that order is the double-entry itself rather than a\n            default the reader is free to change. */}\n        <table className=\"w-full\" data-sortable=\"false\">\n          {/* A width *preference*, not a fixed layout. Auto layout still honours\n              each money column's content minimum, so a figure can never be\n              clipped; the percentages only stop the account column from being\n              squeezed to three wrapped lines while the credit column sits half\n              empty. `table-fixed` does the opposite and clips the money. */}\n          <colgroup>\n            <col className=\"w-[44%]\" />\n            <col className=\"w-[20%]\" />\n            <col className=\"w-[18%]\" />\n            <col className=\"w-[18%]\" />\n          </colgroup>\n          <caption className=\"sr-only\">\n            Journal entry {entry.entryNumber}, {entry.lines.length} lines\n          </caption>\n          <thead>\n            <tr className=\"border-b border-border-subtle\">\n              <th className=\"px-6 py-2 text-left text-label font-medium text-text-secondary\" scope=\"col\">\n                Account\n              </th>\n              <th className=\"px-3 py-2 text-left text-label font-medium text-text-secondary\" scope=\"col\">\n                Dimensions\n              </th>\n              <th className=\"px-3 py-2 text-right text-label font-medium text-text-secondary\" scope=\"col\">\n                Debit\n              </th>\n              <th className=\"px-6 py-2 text-right text-label font-medium text-text-secondary\" scope=\"col\">\n                Credit\n              </th>\n            </tr>\n          </thead>\n          <tbody>\n            {entry.lines.map((line) => (\n              <tr className=\"border-b border-border-subtle\" key={line.id}>\n                <th className=\"px-6 py-2 text-left font-normal\" scope=\"row\">\n                  <span className=\"reference-numeric text-caption text-text-tertiary\">\n                    {line.accountNumber}\n                  </span>{\" \"}\n                  <span className=\"text-body\">{line.accountName}</span>\n                  {line.memo ? (\n                    // A line memo is auditor-relevant but rarely the thing being\n                    // scanned. It keeps its own line and truncates; the full text\n                    // is one hover away rather than three wrapped lines deep.\n                    <Tooltip content={line.memo} display=\"block\">\n                      <span className=\"block truncate text-caption text-text-secondary\">{line.memo}</span>\n                    </Tooltip>\n                  ) : null}\n                </th>\n                <td className=\"px-3 py-2 text-left text-caption text-text-secondary\">\n                  {dimensionSummary(line.dimensions)}\n                </td>\n                <td className=\"px-3 py-2 text-right align-top\">\n                  {line.debit ? <Money accounting value={line.debit} /> : null}\n                </td>\n                <td className=\"px-6 py-2 text-right align-top\">\n                  {line.credit ? <Money accounting value={line.credit} /> : null}\n                </td>\n              </tr>\n            ))}\n          </tbody>\n          <tfoot>\n            <tr>\n              <td colSpan={2} />\n              <td className=\"px-3\">\n                <Rule variant=\"subtotal\" />\n              </td>\n              <td className=\"px-6\">\n                <Rule variant=\"subtotal\" />\n              </td>\n            </tr>\n            <tr>\n              <th className=\"px-6 py-2 text-left text-label font-medium\" colSpan={2} scope=\"row\">\n                Totals\n              </th>\n              <td className=\"px-3 py-2 text-right\">\n                <Money accounting size=\"md\" value={totalDebit} />\n              </td>\n              <td className=\"px-6 py-2 text-right\">\n                <Money accounting size=\"md\" value={totalCredit} />\n              </td>\n            </tr>\n            <tr>\n              <td colSpan={2} />\n              <td className=\"px-3\">\n                <Rule variant=\"total\" />\n              </td>\n              <td className=\"px-6\">\n                <Rule variant=\"total\" />\n              </td>\n            </tr>\n          </tfoot>\n        </table>\n\n        <div className=\"space-y-2 px-6 py-4\">\n          <p\n            className={cn(\n              \"flex items-center gap-2 rounded-sm px-3 py-2 text-body\",\n              balanced ? \"bg-positive-subtle text-positive\" : \"bg-negative-subtle text-negative\",\n            )}\n            role={balanced ? undefined : \"alert\"}\n          >\n            {balanced ? (\n              <CheckIcon aria-hidden=\"true\" className=\"size-4 shrink-0\" />\n            ) : (\n              <ExclamationTriangleIcon aria-hidden=\"true\" className=\"size-4 shrink-0\" />\n            )}\n            {balanced ? (\n              \"Balanced.\"\n            ) : (\n              <>\n                Out of balance by <Money tone=\"inherit\" value={{ amount: Math.abs(difference), currency }} />\n                <InfoTip\n                  about=\"What an out-of-balance entry means\"\n                  label=\"This entry cannot post\"\n                  triggerClassName=\"text-negative\"\n                >\n                  Debits and credits must agree to the cent before an entry reaches the ledger. Correct a\n                  line, or add the balancing one.\n                </InfoTip>\n              </>\n            )}\n          </p>\n\n          {/* One reversal line, not three stacked sentences. Each fact is short\n              enough to sit beside the others; the one piece of doctrine —\n              that reversal never removes the original — is the tooltip. */}\n          {entry.autoReverseOn || entry.reversalOf || entry.reversedBy ? (\n            <p className=\"flex flex-wrap items-center gap-x-2 gap-y-1 text-caption text-text-secondary\">\n              <ArrowUturnLeftIcon aria-hidden=\"true\" className=\"size-3.5 shrink-0\" />\n              {entry.autoReverseOn ? (\n                <span>\n                  Reverses <span className=\"numeric\">{formatDate(entry.autoReverseOn)}</span>\n                </span>\n              ) : null}\n              {entry.reversalOf ? (\n                <>\n                  {entry.autoReverseOn ? <span aria-hidden=\"true\">·</span> : null}\n                  <span>Reverses {entry.reversalOf}</span>\n                </>\n              ) : null}\n              {entry.reversedBy ? (\n                <>\n                  {entry.autoReverseOn || entry.reversalOf ? <span aria-hidden=\"true\">·</span> : null}\n                  <span>Reversed by {entry.reversedBy}</span>\n                </>\n              ) : null}\n              <InfoTip about=\"How reversal works\" label=\"Reversal, not deletion\">\n                A posted entry is never edited or removed. The original and its reversal both stay in the\n                ledger, and the audit trail shows both.\n              </InfoTip>\n            </p>\n          ) : null}\n\n          {entry.evidence?.length ? (\n            <ul className=\"flex flex-wrap gap-2\">\n              {entry.evidence.map((e) => (\n                <li\n                  className=\"inline-flex items-center gap-1.5 rounded-sm border border-border px-2 py-1 text-caption\"\n                  key={e.name}\n                >\n                  <PaperClipIcon aria-hidden=\"true\" className=\"size-3 text-text-tertiary\" />\n                  {e.name}\n                  <span className=\"reference-numeric text-text-tertiary\">{e.hash.slice(0, 8)}</span>\n                </li>\n              ))}\n            </ul>\n          ) : null}\n        </div>\n      </article>\n    );\n  },\n);\nJournalEntry.displayName = \"JournalEntry\";\n\nfunction dimensionSummary(dimensions: Record<string, string | undefined>): string {\n  return Object.entries(dimensions)\n    .filter(([, v]) => v)\n    .map(([, v]) => v)\n    .join(\" · \");\n}\n",
      "path": "components/ledger/journal-entry.tsx",
      "target": "components/ledger/journal-entry.tsx",
      "type": "registry:component"
    }
  ],
  "name": "journal-entry",
  "registryDependencies": [
    "@lopifi/theme",
    "@lopifi/accounting.types",
    "@lopifi/cn",
    "@lopifi/fiscal-period",
    "@lopifi/icons",
    "@lopifi/info-tip",
    "@lopifi/money",
    "@lopifi/money.types",
    "@lopifi/reference",
    "@lopifi/rule",
    "@lopifi/tooltip"
  ],
  "title": "JournalEntry",
  "type": "registry:component"
}
