{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "bitcoin-motion", "title": "Bitcoin motion", "description": "Accessible confirmation, mempool, proof-of-work, UTXO, and Merkle motion primitives.", "registryDependencies": [ "dennisonbertram/bitcoin-ui/bitcoin-core", "dennisonbertram/bitcoin-ui/bitcoin-theme" ], "files": [ { "path": "components/bitcoin/bitcoin-motion.tsx", "content": "\"use client\";\n\nimport {\n useId,\n useState,\n type ComponentProps,\n type CSSProperties,\n} from \"react\";\n\nimport {\n clampPercent,\n formatFeeRate,\n formatSats,\n formatWeight,\n truncateMiddle,\n type SatoshiValue,\n} from \"@/lib/bitcoin\";\nimport { componentClasses } from \"@/lib/utils\";\n\nimport styles from \"./bitcoin-motion.module.css\";\nimport type { BitcoinVisualProps } from \"./shared\";\n\ntype MotionProps = BitcoinVisualProps & {\n /** Pauses motion without changing the represented state. @default false */\n paused?: boolean;\n};\n\ntype MotionStyle = CSSProperties & {\n \"--motion-column\"?: number;\n \"--motion-index\"?: number;\n \"--motion-lane\"?: number;\n};\n\nexport type AnimatedConfirmationsProps = Omit<\n ComponentProps<\"div\">,\n \"children\"\n> &\n MotionProps & {\n confirmations: number;\n /** Confirmation target represented by the relay. @default 6 */\n target?: number;\n };\n\nexport function AnimatedConfirmations({\n confirmations,\n target = 6,\n paused,\n unstyled,\n className,\n ...props\n}: AnimatedConfirmationsProps) {\n const safeTarget = Math.max(1, Math.floor(target));\n const safeConfirmations = Math.max(0, Math.floor(confirmations));\n const boundedConfirmations = Math.min(safeConfirmations, safeTarget);\n const remaining = Math.max(0, safeTarget - boundedConfirmations);\n const complete = boundedConfirmations >= safeTarget;\n const valueText = complete\n ? `${safeConfirmations} confirmations, finality target reached`\n : `${boundedConfirmations} of ${safeTarget} confirmations`;\n\n return (\n \n
\n Confirmation relay\n \n {boundedConfirmations} / {safeTarget}\n \n
\n \n \n {Array.from({ length: safeTarget }, (_, index) => {\n const confirmed = index < boundedConfirmations;\n\n return (\n \n {index + 1}\n \n );\n })}\n \n \n

\n {complete\n ? \"Finality target reached.\"\n : `${remaining} ${remaining === 1 ? \"block\" : \"blocks\"} until the display target.`}\n

\n \n );\n}\n\nexport type CandidateBlockMetadata = {\n /** Label shown at the top of the metadata surface. @default \"Candidate block\" */\n label?: string;\n /** Current assembly state. @default \"Assembling\" */\n status?: string;\n /** Transactions currently selected for the candidate block. */\n transactionCount?: number;\n /** Candidate block weight in weight units. */\n weight?: number;\n /** Aggregate transaction fees selected for the candidate block. */\n feeTotal?: SatoshiValue;\n};\n\nexport type MempoolPacketMetadata = {\n /** Optional sample label. @default \"Transaction sample\" */\n label?: string;\n /** Caller-supplied transaction id for this sample. */\n txid?: string;\n /** Fee rate in sat/vB. */\n feeRate?: number;\n /** Virtual transaction size in vbytes. */\n vsize?: number;\n /** Queue state. @default \"Waiting\" */\n status?: string;\n};\n\nexport type MempoolFlowProps = Omit, \"children\"> &\n MotionProps & {\n transactionCount: number;\n /** Normalized pressure from 0 to 1. */\n pressure: number;\n /** Number of visual packets. Clamped from 8 to 32. @default 20 */\n packetCount?: number;\n /**\n * Transaction samples represented across the animated field and exposed\n * through a keyboard- and touch-accessible inspector.\n */\n packetSamples?: readonly MempoolPacketMetadata[];\n /** Optional caller-supplied metadata for the candidate block preview. */\n candidateBlock?: CandidateBlockMetadata;\n };\n\nexport function MempoolFlow({\n transactionCount,\n pressure,\n packetCount = 20,\n packetSamples = [],\n candidateBlock,\n paused,\n unstyled,\n className,\n ...props\n}: MempoolFlowProps) {\n const metadataId = useId();\n const sampleInspectorId = useId();\n const [metadataOpen, setMetadataOpen] = useState(false);\n const [selectedSampleIndex, setSelectedSampleIndex] = useState(0);\n const safeCount = Math.max(0, Math.floor(transactionCount));\n const normalizedPressure = clampPercent(pressure * 100);\n const visualPackets = Math.min(32, Math.max(8, Math.floor(packetCount)));\n const filledSlots = Math.max(1, Math.round(normalizedPressure / 25));\n const metadataLabel = candidateBlock?.label ?? \"Candidate block\";\n const metadataStatus = candidateBlock?.status ?? \"Assembling\";\n const activeSample =\n packetSamples.length > 0\n ? packetSamples[\n Math.min(selectedSampleIndex, packetSamples.length - 1)\n ]\n : undefined;\n const candidateTransactionCount =\n candidateBlock?.transactionCount === undefined\n ? undefined\n : Math.max(0, Math.floor(candidateBlock.transactionCount));\n const label = `${new Intl.NumberFormat(\"en-US\").format(\n safeCount,\n )} transactions at ${Math.round(normalizedPressure)} percent pressure, flowing toward the next block`;\n\n return (\n = 75\n ? \"high\"\n : normalizedPressure >= 40\n ? \"medium\"\n : \"low\"\n }\n data-paused={paused || undefined}\n data-unstyled={unstyled || undefined}\n role=\"group\"\n aria-label={label}\n className={componentClasses(unstyled, styles.mempool, className)}\n {...props}\n >\n \n \n Mempool pressure\n {packetSamples.length > 0 ? (\n \n Inspect samples below\n \n ) : null}\n \n {Math.round(normalizedPressure)}%\n \n
\n \n {Array.from({ length: visualPackets }, (_, index) => {\n const lane = index % 4;\n const sample =\n packetSamples.length > 0\n ? packetSamples[index % packetSamples.length]\n : undefined;\n\n return (\n \n \n \n
\n \n setMetadataOpen((open) => !open)}\n onKeyDown={(event) => {\n if (event.key === \"Escape\") {\n setMetadataOpen(false);\n }\n }}\n >\n \n Next block\n Inspect\n \n \n {Array.from({ length: 4 }, (_, index) => (\n \n ))}\n \n \n