{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "send", "type": "registry:block", "title": "Send block", "description": "Stage-then-confirm rendered: form, arithmetic-ladder review, a commit button that restates the amount, single execution on a pinned idempotency key, status timeline.", "dependencies": [ "@venlyfinance/react@^0.2.0", "@venlyfinance/sdk@^0.3.0", "@tanstack/react-query@^5.0.0" ], "registryDependencies": [ "@venlyfinance/venly-tokens", "@venlyfinance/money", "@venlyfinance/arithmetic-ladder", "@venlyfinance/timeline" ], "files": [ { "path": "registry/blocks/send.tsx", "type": "registry:component", "target": "~/components/venly/blocks/send.tsx", "content": "import { useState, type CSSProperties, type ReactElement } from \"react\";\nimport {\n useStagedTransfer,\n type StagedTransferState,\n type TransferDraft,\n} from \"@venlyfinance/react\";\nimport { formatAmount } from \"../lib/money.js\";\nimport { ArithmeticLadder, type LadderRow } from \"../components/arithmetic-ladder.js\";\nimport { Timeline, type TimelineStep } from \"../components/timeline.js\";\n\n/**\n * Send block – stage-then-confirm, rendered.\n *\n * Design contract encoded by this block:\n * - Money movement is never form-submit-to-execution: the review step\n * renders the EXACT staged request (the arithmetic ladder), and only\n * confirm() executes – once, on a pinned idempotency key.\n * - The commit button restates the amount: \"Pay €1,240.00\". Never a\n * countdown on the button.\n * - Values are never masked on a review screen.\n * - After confirm, the transfer's status story renders as the timeline;\n * failure is a terminal state with the reason, not a toast.\n */\n\nconst inputStyle: CSSProperties = {\n width: \"100%\",\n boxSizing: \"border-box\",\n border: \"var(--border-w-hairline) solid var(--border-strong)\",\n borderRadius: \"var(--radius-control)\",\n padding: \"var(--space-sm) var(--space-md)\",\n fontSize: \"var(--font-size-body)\",\n fontFamily: \"var(--font-family)\",\n color: \"var(--text-primary)\",\n background: \"var(--surface-raised)\",\n};\n\nconst labelStyle: CSSProperties = {\n display: \"block\",\n fontSize: \"var(--font-size-label)\",\n color: \"var(--text-secondary)\",\n marginBottom: \"var(--space-2xs)\",\n};\n\nexport interface SendFormValues {\n receiverAccountId: string;\n currency: string;\n amount: string;\n description: string;\n}\n\n/**\n * Amount input guard. Number(\"\")/Number(\" \") are 0 and Number(\"Infinity\")\n * is > 0, so a falsy/NaN check alone lets nonsense reach the review step.\n * Returns the parsed amount, or null when the input must not stage.\n */\nexport function parseAmountInput(raw: string): number | null {\n const trimmed = raw.trim();\n if (!trimmed) return null;\n const amount = Number(trimmed);\n return Number.isFinite(amount) && amount > 0 ? amount : null;\n}\n\nexport function SendForm({\n values,\n issues,\n onChange,\n onStage,\n}: {\n values: SendFormValues;\n issues: string[];\n onChange: (values: SendFormValues) => void;\n onStage: () => void;\n}): ReactElement {\n return (\n
\n );\n}\n\nconst primaryButton: CSSProperties = {\n border: \"none\",\n background: \"var(--accent)\",\n color: \"var(--accent-fg)\",\n borderRadius: \"var(--radius-control)\",\n padding: \"var(--space-sm) var(--space-lg)\",\n fontSize: \"var(--font-size-body)\",\n fontWeight: 500,\n cursor: \"pointer\",\n justifySelf: \"start\",\n};\n\nconst quietButton: CSSProperties = {\n border: \"var(--border-w-hairline) solid var(--border-strong)\",\n background: \"var(--surface-raised)\",\n color: \"var(--text-primary)\",\n borderRadius: \"var(--radius-control)\",\n padding: \"var(--space-sm) var(--space-lg)\",\n fontSize: \"var(--font-size-body)\",\n fontWeight: 500,\n cursor: \"pointer\",\n};\n\nexport function SendReview({\n draft,\n fee,\n onConfirm,\n onEdit,\n submitting,\n}: {\n draft: TransferDraft;\n /** Fee amount when known; renders as an estimate row. */\n fee?: number;\n onConfirm: () => void;\n onEdit: () => void;\n submitting?: boolean;\n}): ReactElement {\n const body = draft.body as { amount?: number; currency?: string; description?: string };\n const amount = body.amount ?? 0;\n const currency = draft.kind === \"fiat\" ? (body.currency ?? \"\") : \"\";\n const rows: LadderRow[] = fee\n ? [{ operator: \"−\", label: \"Transfer fee\", amount: fee, currency, uncertain: true }]\n : [];\n const net = fee ? amount - fee : amount;\n\n return (\n