{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "fee-estimates", "title": "Fee estimates", "description": "Controlled or uncontrolled block-target fee selection.", "dependencies": [ "lucide-react" ], "registryDependencies": [ "dennisonbertram/bitcoin-ui/bitcoin-core", "dennisonbertram/bitcoin-ui/bitcoin-theme" ], "files": [ { "path": "components/bitcoin/fee-estimates.tsx", "content": "\"use client\";\n\nimport { Check } from \"lucide-react\";\nimport {\n type ComponentProps,\n type KeyboardEvent,\n useId,\n useMemo,\n useRef,\n useState,\n} from \"react\";\n\nimport { formatFeeRate, type FeeEstimate } from \"@/lib/bitcoin\";\nimport { componentClasses } from \"@/lib/utils\";\n\nimport {\n interactiveStyles,\n panelStyles,\n type BitcoinVisualProps,\n} from \"./shared\";\n\nexport type FeeEstimatesProps = Omit, \"children\"> &\n BitcoinVisualProps & {\n estimates: FeeEstimate[];\n selectedBlocks?: number;\n defaultSelectedBlocks?: number;\n onSelectionChange?: (estimate: FeeEstimate) => void;\n disabled?: boolean;\n };\n\nexport function FeeEstimates({\n estimates,\n selectedBlocks,\n defaultSelectedBlocks,\n onSelectionChange,\n disabled,\n unstyled,\n className,\n ...props\n}: FeeEstimatesProps) {\n const groupLabelId = useId();\n const initial = defaultSelectedBlocks ?? estimates[0]?.blocks;\n const [internalSelection, setInternalSelection] = useState(initial);\n const optionRefs = useRef>([]);\n const currentSelection = selectedBlocks ?? internalSelection;\n const sorted = useMemo(\n () => [...estimates].sort((a, b) => a.blocks - b.blocks),\n [estimates],\n );\n\n function chooseEstimate(estimate: FeeEstimate) {\n if (disabled) return;\n if (selectedBlocks === undefined) setInternalSelection(estimate.blocks);\n onSelectionChange?.(estimate);\n }\n\n function handleOptionKeyDown(\n event: KeyboardEvent,\n index: number,\n ) {\n if (disabled || sorted.length === 0) return;\n\n let nextIndex: number;\n switch (event.key) {\n case \"ArrowDown\":\n case \"ArrowRight\":\n nextIndex = (index + 1) % sorted.length;\n break;\n case \"ArrowUp\":\n case \"ArrowLeft\":\n nextIndex = (index - 1 + sorted.length) % sorted.length;\n break;\n case \"Home\":\n nextIndex = 0;\n break;\n case \"End\":\n nextIndex = sorted.length - 1;\n break;\n default:\n return;\n }\n\n event.preventDefault();\n chooseEstimate(sorted[nextIndex]);\n optionRefs.current[nextIndex]?.focus();\n }\n\n return (\n \n \n Fee estimate\n \n {sorted.map((estimate, index) => {\n const selected = currentSelection === estimate.blocks;\n const hasSelectedOption = sorted.some(\n (option) => option.blocks === currentSelection,\n );\n const minutes = estimate.minutes ?? estimate.blocks * 10;\n\n return (\n {\n optionRefs.current[index] = node;\n }}\n onClick={() => chooseEstimate(estimate)}\n onKeyDown={(event) => handleOptionKeyDown(event, index)}\n className={componentClasses(\n unstyled,\n [\n interactiveStyles,\n \"grid min-h-14 grid-cols-[minmax(0,1fr)_auto] items-center gap-4 rounded-[var(--radius-md)] px-3 py-2 text-left hover:bg-[var(--color-surface-hover)] data-[state=selected]:bg-[var(--color-surface-active)]\",\n ],\n )}\n >\n \n \n {estimate.label}\n {selected ? (\n \n ) : null}\n \n \n ~{minutes} min ยท {estimate.blocks}{\" \"}\n {estimate.blocks === 1 ? \"block\" : \"blocks\"}\n \n \n \n {formatFeeRate(estimate.satPerVbyte)}\n \n \n );\n })}\n \n );\n}\n", "type": "registry:component" } ], "type": "registry:component" }