{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "approval-page",
  "type": "registry:block",
  "title": "Approval page",
  "description": "Review queue, evidence, required explanation, confirmation and guarded decision write.",
  "registryDependencies": [
    "button",
    "alert-dialog",
    "https://shine-blond.vercel.app/r/data-grid.json"
  ],
  "files": [
    {
      "path": "blocks/approval-page.tsx",
      "type": "registry:component",
      "target": "components/shine/approval-page.tsx",
      "content": "\"use client\";\nimport { useId, useRef, useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction } from \"@/components/ui/alert-dialog\";\nimport { DataGrid } from \"@/components/shine/data-grid\";\nexport type ApprovalRecord = { id: string; title: string; owner: string; status: string; evidence: string };\nexport function ApprovalPage({ title, description, records, onDecision, onRetry, error, loading = false }: {\n title: string; description: string; records: ApprovalRecord[]; onDecision: (record: ApprovalRecord, decision: \"approve\" | \"reject\", reason: string) => Promise<void>; onRetry: () => void; error?: string; loading?: boolean;\n}) {\n const id = useId(), lock = useRef(false), [selectedId, setSelectedId] = useState<string | null>(null), [drafts, setDrafts] = useState<Record<string, string>>({}), [decision, setDecision] = useState<\"approve\" | \"reject\" | null>(null), [busy, setBusy] = useState(false), [failure, setFailure] = useState(\"\"), [notice, setNotice] = useState(\"\");\n const selected = records.find(record => record.id === selectedId);\n const reason = selectedId ? drafts[selectedId] || \"\" : \"\";\n const setReason = (value: string) => { if (selectedId) setDrafts(current => ({ ...current, [selectedId]: value })); };\n async function commit() { if (!selected || !decision || lock.current) return; if (!reason.trim()) { setFailure(\"Explain your decision before submitting.\"); setDecision(null); return; } lock.current = true; setBusy(true); setFailure(\"\"); try { await onDecision(selected, decision, reason.trim()); setNotice(selected.title + \": decision saved.\"); setSelectedId(null); setReason(\"\"); setDecision(null); } catch (cause) { setFailure(cause instanceof Error ? cause.message : \"Decision failed. Your review remains available.\"); setDecision(null); } finally { lock.current = false; setBusy(false); } }\n return <main data-product-pattern=\"approval-page\" data-cite=\"shadcn-queue\" className=\"mx-auto max-w-7xl space-y-6 p-4 sm:p-6\"><header className=\"space-y-2\"><h1 className=\"text-2xl font-semibold tracking-tight\">{title}</h1><p className=\"text-muted-foreground\">{description}</p></header><p role=\"status\">{notice}</p><DataGrid title=\"Review queue\" rows={records} columns={[{ accessorKey: \"title\", header: \"Record\" }, { accessorKey: \"owner\", header: \"Owner\" }, { accessorKey: \"status\", header: \"Status\" }]} columnLabels={{ title: \"Record\", owner: \"Owner\", status: \"Status\" }} getRowId={record => record.id} rowLabel={record => record.title} onOpen={record => { if (lock.current) return; setSelectedId(record.id); setFailure(\"\"); }} onRetry={onRetry} state={loading ? \"loading\" : error ? \"error\" : \"ready\"} errorMessage={error} emptyMessage=\"Nothing needs review.\" summary={<p>{records.length} records available for review</p>} />\n  {selected && <section aria-labelledby={id} className=\"space-y-4 rounded-lg border border-border p-5\"><h2 id={id} className=\"text-lg font-semibold\">Review {selected.title}</h2><p>{selected.evidence || \"No supporting evidence recorded.\"}</p><label className=\"block space-y-2\"><span className=\"text-sm font-medium\">Decision reason (required)</span><textarea aria-label=\"Decision reason (required)\" className=\"w-full rounded-md border border-border bg-background p-3\" rows={4} value={reason} disabled={busy} onChange={e => setReason(e.target.value)} /></label><p role=\"alert\" className=\"text-sm text-destructive\">{failure}</p><div className=\"flex flex-wrap gap-2\"><Button disabled={busy} type=\"button\" onClick={() => { if (!reason.trim()) setFailure(\"Explain your decision before submitting.\"); else setDecision(\"approve\"); }}>Approve</Button><Button disabled={busy} variant=\"outline\" type=\"button\" onClick={() => { if (!reason.trim()) setFailure(\"Explain your decision before submitting.\"); else setDecision(\"reject\"); }}>Reject</Button><Button disabled={busy} variant=\"ghost\" type=\"button\" onClick={() => { setSelectedId(null); }}>Close review</Button></div></section>}\n  <AlertDialog open={Boolean(decision)} onOpenChange={open => { if (!open && !busy) setDecision(null); }}><AlertDialogContent onEscapeKeyDown={event => { if (busy) event.preventDefault(); }}><AlertDialogHeader><AlertDialogTitle>{decision === \"approve\" ? \"Approve this record?\" : \"Reject this record?\"}</AlertDialogTitle><AlertDialogDescription>Your decision and explanation will be saved.</AlertDialogDescription></AlertDialogHeader><AlertDialogFooter><AlertDialogCancel disabled={busy}>Keep reviewing</AlertDialogCancel><AlertDialogAction disabled={busy} onClick={event => { event.preventDefault(); void commit(); }}>{busy ? \"Saving…\" : \"Confirm decision\"}</AlertDialogAction></AlertDialogFooter></AlertDialogContent></AlertDialog></main>;\n}\n"
    }
  ]
}
