{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "author": "Lopifi (https://lopifi.com)",
  "categories": [
    "payables-ap",
    "finance",
    "accounting"
  ],
  "dependencies": [],
  "description": "Mobile-first, document inline.",
  "files": [
    {
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/ledger/cn\";\nimport { formatDate } from \"@/lib/ledger/fiscal-period\";\nimport type { MoneyValue } from \"@/lib/ledger/money.types\";\nimport { Button } from \"@/components/ledger/button\";\nimport { EntityMark } from \"@/components/ledger/entity-mark\";\nimport { CheckIcon, ChevronDownIcon, DocumentTextIcon, XMarkIcon } from \"@/components/ledger/icons\";\nimport { Money } from \"@/components/ledger/money\";\n\nexport type ApprovalItem = {\n  id: string;\n  vendor: string;\n  amount: MoneyValue;\n  dueDate: Date;\n  coding: string;\n  /** The source document, readable at 390px. */\n  documentName: string;\n  documentPreview?: string;\n};\n\nexport interface ApprovalInboxProps {\n  className?: string;\n  items: ApprovalItem[];\n  onApprove?: (id: string) => void;\n  /** Rejection requires a reason and routes back to the preparer with it. */\n  onReject?: (id: string, reason: string) => void;\n}\n\n/**\n * **Mobile-first.** Approvals happen on phones between meetings, so the\n * source document has to be readable at 390px. Swipe actions have an\n * explicit button equivalent, because swipe is undiscoverable.\n *\n * An approver who has to click through to see what they're approving will\n * stop clicking through — so the document is inline.\n */\nexport function ApprovalInbox({ items, onApprove, onReject, className }: ApprovalInboxProps) {\n  const [rejecting, setRejecting] = React.useState<string | null>(null);\n  const [reason, setReason] = React.useState(\"\");\n  const [expanded, setExpanded] = React.useState<string | null>(items[0]?.id ?? null);\n\n  return (\n    <ul className={cn(\"space-y-3\", className)}>\n      {items.map((item) => (\n        <li className=\"overflow-hidden rounded-xl border border-border bg-surface\" key={item.id}>\n          <div className=\"flex items-start gap-3 p-4\">\n            <EntityMark category=\"company\" name={item.vendor} size={40} />\n            <div className=\"min-w-0 flex-1\">\n              <p className=\"truncate text-body font-medium\">{item.vendor}</p>\n              <p className=\"text-caption text-text-secondary\">\n                Due <span className=\"numeric\">{formatDate(item.dueDate)}</span>\n              </p>\n              <p className=\"text-caption text-text-secondary\">{item.coding}</p>\n            </div>\n            <Money size=\"md\" value={item.amount} />\n          </div>\n\n          <button\n            aria-expanded={expanded === item.id}\n            className=\"flex w-full items-center gap-2 border-t border-border-subtle px-4 py-2 text-label font-medium text-accent\"\n            onClick={() => setExpanded(expanded === item.id ? null : item.id)}\n            type=\"button\"\n          >\n            <DocumentTextIcon aria-hidden=\"true\" className=\"size-3.5\" />\n            {item.documentName}\n            <ChevronDownIcon\n              aria-hidden=\"true\"\n              className={cn(\n                \"ml-auto size-4 transition-transform duration-2 ease-standard\",\n                expanded === item.id && \"rotate-180\",\n              )}\n            />\n          </button>\n\n          {expanded === item.id ? (\n            <div className=\"border-t border-border-subtle bg-surface-sunken p-4\">\n              <div className=\"min-h-[160px] rounded-sm border border-border bg-surface p-4\">\n                <p className=\"whitespace-pre-wrap text-caption text-text-secondary\">\n                  {item.documentPreview ??\n                    `${item.vendor}\\nInvoice document\\n\\nThis renders inline so the approver can read what they are approving without leaving the inbox.`}\n                </p>\n              </div>\n            </div>\n          ) : null}\n\n          {rejecting === item.id ? (\n            <div className=\"space-y-2 border-t border-border-subtle p-4\">\n              <label className=\"block\">\n                <span className=\"mb-1 block text-label font-medium text-text-secondary\">\n                  Reason for rejection (required — the preparer sees this)\n                </span>\n                <textarea\n                  className=\"w-full rounded-sm border border-border-control bg-surface p-2 text-body outline-none focus-visible:border-accent\"\n                  onChange={(e) => setReason(e.target.value)}\n                  rows={2}\n                  value={reason}\n                />\n              </label>\n              <div className=\"flex gap-2\">\n                <Button\n                  disabled={reason.trim().length < 5}\n                  onClick={() => {\n                    onReject?.(item.id, reason);\n                    setRejecting(null);\n                    setReason(\"\");\n                  }}\n                  variant=\"destructive\"\n                >\n                  Reject and return to preparer\n                </Button>\n                <Button onClick={() => setRejecting(null)} variant=\"ghost\">\n                  Cancel\n                </Button>\n              </div>\n            </div>\n          ) : (\n            <div\n              className=\"flex gap-2 border-t border-border-subtle p-4\"\n              style={{ paddingBottom: \"max(16px, env(safe-area-inset-bottom))\" }}\n            >\n              <Button className=\"flex-1\" onClick={() => onApprove?.(item.id)} variant=\"primary\">\n                <CheckIcon aria-hidden=\"true\" className=\"size-4\" />\n                Approve\n              </Button>\n              <Button className=\"flex-1\" onClick={() => setRejecting(item.id)} variant=\"secondary\">\n                <XMarkIcon aria-hidden=\"true\" className=\"size-4\" />\n                Reject\n              </Button>\n            </div>\n          )}\n        </li>\n      ))}\n    </ul>\n  );\n}\n",
      "path": "components/ledger/approval-inbox.tsx",
      "target": "components/ledger/approval-inbox.tsx",
      "type": "registry:component"
    }
  ],
  "meta": {
    "license": "MIT"
  },
  "name": "approval-inbox",
  "registryDependencies": [
    "@lopifi/theme",
    "@lopifi/button",
    "@lopifi/cn",
    "@lopifi/entity-mark",
    "@lopifi/fiscal-period",
    "@lopifi/icons",
    "@lopifi/money",
    "@lopifi/money.types"
  ],
  "title": "ApprovalInbox",
  "type": "registry:component"
}
