{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "bitcoin-core", "title": "Bitcoin UI core", "description": "Types, exact formatting utilities, class helpers, and shared component styles.", "dependencies": [ "clsx", "tailwind-merge" ], "files": [ { "path": "lib/bitcoin.ts", "content": "export type BitcoinNetwork = \"mainnet\" | \"testnet\" | \"signet\" | \"regtest\";\n\nexport type ScriptType =\n | \"p2pkh\"\n | \"p2sh\"\n | \"p2wpkh\"\n | \"p2wsh\"\n | \"p2tr\"\n | \"op-return\"\n | \"unknown\";\n\nexport type TransactionState =\n | \"confirmed\"\n | \"pending\"\n | \"replaced\"\n | \"conflicted\";\n\nexport type AmountUnit = \"sat\" | \"btc\" | \"auto\";\n\nexport type BitcoinSearchKind =\n | \"block-height\"\n | \"hash\"\n | \"address\"\n | \"unknown\";\n\nexport type SatoshiValue = bigint | number | string;\n\nexport interface BitcoinBlock {\n height: number;\n hash: string;\n timestamp: number | Date;\n transactionCount: number;\n size: number;\n weight: number;\n miner?: string;\n feeTotal?: SatoshiValue;\n}\n\nexport interface BitcoinTransaction {\n txid: string;\n value: SatoshiValue;\n fee: SatoshiValue;\n vsize: number;\n timestamp?: number | Date;\n confirmations?: number;\n state: TransactionState;\n}\n\nexport interface BitcoinUtxo {\n txid: string;\n vout: number;\n value: SatoshiValue;\n confirmations: number;\n scriptType: ScriptType;\n address?: string;\n spendable?: boolean;\n}\n\nexport interface TransactionEndpoint {\n id: string;\n address?: string;\n label?: string;\n value: SatoshiValue;\n scriptType?: ScriptType;\n coinbase?: boolean;\n}\n\nexport interface FeeEstimate {\n label: string;\n blocks: number;\n satPerVbyte: number;\n minutes?: number;\n}\n\nconst SATOSHIS_PER_BTC = BigInt(100_000_000);\n\nexport function toSatoshis(value: SatoshiValue): bigint {\n if (typeof value === \"bigint\") return value;\n\n if (typeof value === \"number\") {\n if (!Number.isFinite(value)) {\n throw new TypeError(\"Satoshi value must be finite.\");\n }\n return BigInt(Math.trunc(value));\n }\n\n if (!/^-?\\d+$/.test(value.trim())) {\n throw new TypeError(\"Satoshi value must be an integer string.\");\n }\n\n return BigInt(value);\n}\n\nexport function formatSats(\n value: SatoshiValue,\n options: Intl.NumberFormatOptions = {},\n) {\n const sats = toSatoshis(value);\n return new Intl.NumberFormat(\"en-US\", {\n maximumFractionDigits: 0,\n ...options,\n }).format(sats);\n}\n\nexport function formatBtc(\n value: SatoshiValue,\n {\n minimumFractionDigits = 0,\n maximumFractionDigits = 8,\n }: {\n minimumFractionDigits?: number;\n maximumFractionDigits?: number;\n } = {},\n) {\n const sats = toSatoshis(value);\n const negative = sats < BigInt(0);\n const absolute = negative ? -sats : sats;\n const whole = absolute / SATOSHIS_PER_BTC;\n const remainder = absolute % SATOSHIS_PER_BTC;\n const fraction = remainder.toString().padStart(8, \"0\");\n const trimmed = fraction\n .slice(0, maximumFractionDigits)\n .replace(/0+$/, \"\")\n .padEnd(minimumFractionDigits, \"0\");\n\n return `${negative ? \"-\" : \"\"}${formatSats(whole)}${\n trimmed ? `.${trimmed}` : \"\"\n }`;\n}\n\nexport function resolveAmountUnit(\n value: SatoshiValue,\n unit: AmountUnit,\n): Exclude {\n if (unit !== \"auto\") return unit;\n return toSatoshis(value) >= BigInt(1_000_000) ? \"btc\" : \"sat\";\n}\n\nexport function truncateMiddle(\n value: string,\n startCharacters = 8,\n endCharacters = 8,\n) {\n if (value.length <= startCharacters + endCharacters + 1) return value;\n return `${value.slice(0, startCharacters)}…${value.slice(-endCharacters)}`;\n}\n\nexport function formatBytes(bytes: number) {\n if (!Number.isFinite(bytes) || bytes < 0) return \"—\";\n if (bytes < 1_000) return `${Math.round(bytes)} B`;\n if (bytes < 1_000_000) return `${(bytes / 1_000).toFixed(1)} kB`;\n return `${(bytes / 1_000_000).toFixed(2)} MB`;\n}\n\nexport function formatWeight(weight: number) {\n if (!Number.isFinite(weight) || weight < 0) return \"—\";\n return `${new Intl.NumberFormat(\"en-US\").format(Math.round(weight))} WU`;\n}\n\nexport function formatFeeRate(satPerVbyte: number) {\n if (!Number.isFinite(satPerVbyte) || satPerVbyte < 0) return \"—\";\n return `${new Intl.NumberFormat(\"en-US\", {\n maximumFractionDigits: 1,\n }).format(satPerVbyte)} sat/vB`;\n}\n\nexport function formatBlockHeight(height: number) {\n return new Intl.NumberFormat(\"en-US\").format(height);\n}\n\nexport function toDate(value: number | Date) {\n if (value instanceof Date) return value;\n return new Date(value < 10_000_000_000 ? value * 1_000 : value);\n}\n\nexport function formatTimestamp(value: number | Date) {\n return new Intl.DateTimeFormat(\"en-US\", {\n timeZone: \"UTC\",\n year: \"numeric\",\n month: \"short\",\n day: \"2-digit\",\n hour: \"2-digit\",\n minute: \"2-digit\",\n hour12: false,\n }).format(toDate(value));\n}\n\nexport function formatRelativeTime(\n value: number | Date,\n now: number | Date = new Date(),\n) {\n const deltaSeconds = Math.round(\n (toDate(value).getTime() - toDate(now).getTime()) / 1_000,\n );\n const absolute = Math.abs(deltaSeconds);\n const formatter = new Intl.RelativeTimeFormat(\"en\", { numeric: \"auto\" });\n\n if (absolute < 60) return formatter.format(deltaSeconds, \"second\");\n if (absolute < 3_600) {\n return formatter.format(Math.round(deltaSeconds / 60), \"minute\");\n }\n if (absolute < 86_400) {\n return formatter.format(Math.round(deltaSeconds / 3_600), \"hour\");\n }\n return formatter.format(Math.round(deltaSeconds / 86_400), \"day\");\n}\n\nexport function getNetworkLabel(network: BitcoinNetwork) {\n return {\n mainnet: \"Mainnet\",\n testnet: \"Testnet\",\n signet: \"Signet\",\n regtest: \"Regtest\",\n }[network];\n}\n\nexport function getScriptLabel(script: ScriptType) {\n return {\n p2pkh: \"P2PKH\",\n p2sh: \"P2SH\",\n p2wpkh: \"P2WPKH\",\n p2wsh: \"P2WSH\",\n p2tr: \"P2TR\",\n \"op-return\": \"OP_RETURN\",\n unknown: \"Unknown\",\n }[script];\n}\n\nexport function clampPercent(value: number) {\n return Math.min(100, Math.max(0, value));\n}\n\nexport function feeRateFrom(fee: SatoshiValue, vsize: number) {\n if (!Number.isFinite(vsize) || vsize <= 0) return 0;\n return Number(toSatoshis(fee)) / vsize;\n}\n\nexport function classifyBitcoinQuery(query: string): BitcoinSearchKind {\n const normalized = query.trim();\n if (/^\\d{1,10}$/.test(normalized)) return \"block-height\";\n if (/^[a-fA-F0-9]{64}$/.test(normalized)) return \"hash\";\n if (\n /^(bc1|tb1|bcrt1)[a-zA-HJ-NP-Z0-9]{8,87}$/i.test(normalized) ||\n /^[123mn2][a-km-zA-HJ-NP-Z1-9]{24,34}$/.test(normalized)\n ) {\n return \"address\";\n }\n return \"unknown\";\n}\n", "type": "registry:lib" }, { "path": "lib/utils.ts", "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n\nexport function componentClasses(\n unstyled: boolean | undefined,\n defaults: ClassValue,\n className?: string,\n) {\n return cn(!unstyled && defaults, className);\n}\n", "type": "registry:lib" }, { "path": "components/bitcoin/shared.ts", "content": "import type { ComponentProps } from \"react\";\n\nexport type BitcoinVisualProps = {\n /**\n * Removes all default visual classes while preserving semantics, data\n * attributes, accessibility behavior, and the consumer's className.\n * @default false\n */\n unstyled?: boolean;\n};\n\nexport type BitcoinDivProps = ComponentProps<\"div\"> & BitcoinVisualProps;\n\nexport const interactiveStyles =\n \"outline-none transition-[opacity,transform,background-color,border-color,color] duration-[var(--dur-base)] ease-[var(--ease-out)] focus-visible:ring-2 focus-visible:ring-[var(--color-focus)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--color-paper)] active:translate-y-px disabled:cursor-not-allowed disabled:opacity-45 aria-disabled:cursor-not-allowed aria-disabled:opacity-45\";\n\nexport const badgeStyles =\n \"inline-flex min-h-6 items-center gap-1.5 rounded-[var(--radius-full)] border px-2 py-0.5 text-xs font-medium leading-none whitespace-nowrap\";\n\nexport const panelStyles =\n \"rounded-[var(--radius-lg)] border border-[var(--color-rule)] bg-[var(--color-surface-raised)]\";\n\nexport const monoStyles =\n \"font-mono text-[0.8125rem] tabular-nums tracking-[-0.01em]\";\n", "type": "registry:lib", "target": "components/bitcoin/shared.ts" } ], "type": "registry:lib" }