{ "name": "odds-display", "type": "registry:ui", "title": "Odds Display", "description": "A selectable price in decimal, fractional or American. Decimal is the source of truth and the others are derived, so a format switch can never change what the bet actually pays.", "category": "Gaming", "dependencies": [ "lucide-react@^1.39.0" ], "registryDependencies": [ "lib-motion", "lib-styles", "lib-utils" ], "files": [ { "path": "components/ui/odds-display.tsx", "type": "registry:ui", "content": "import type { ComponentProps, ReactNode } from 'react'\nimport { ArrowDown, ArrowUp } from 'lucide-react'\nimport { enterFade } from '@/lib/motion'\nimport { focusRing, interactive, radius } from '@/lib/styles'\nimport { cn } from '@/lib/utils'\n\n/**\n * A selectable odds button that shows which way the price moved.\n *\n * Three formats, converted here rather than by the caller. Decimal, fractional\n * and American describe the same price and every operator has to render all\n * three for different markets — doing the conversion in each call site is how\n * they end up disagreeing.\n *\n * A drift is flagged with an arrow and a colour, which is the one thing a\n * bettor is watching. It is deliberately not animated: odds boards that flash\n * are unusable when several markets move at once.\n */\nexport type OddsFormat = 'decimal' | 'fractional' | 'american'\n\n/** Decimal is the source of truth; the others are derived from it. */\nfunction formatOdds(decimal: number, format: OddsFormat) {\n if (format === 'decimal') return decimal.toFixed(2)\n\n if (format === 'american') {\n if (decimal >= 2) return `+${Math.round((decimal - 1) * 100)}`\n return `${Math.round(-100 / (decimal - 1))}`\n }\n\n // Fractional: reduce (decimal - 1) to a simple fraction.\n const profit = decimal - 1\n let bestNumerator = 1\n let bestDenominator = 1\n let bestError = Infinity\n for (let denominator = 1; denominator <= 20; denominator++) {\n const numerator = Math.round(profit * denominator)\n const error = Math.abs(profit - numerator / denominator)\n if (error < bestError) {\n bestError = error\n bestNumerator = numerator\n bestDenominator = denominator\n }\n }\n return `${bestNumerator}/${bestDenominator}`\n}\n\nfunction OddsDisplay({\n label,\n odds,\n previousOdds,\n format = 'decimal',\n selected = false,\n suspended = false,\n onSelect,\n driftedLabel = 'drifted out',\n shortenedLabel = 'shortened',\n className,\n ...props\n}: Omit, 'onSelect'> & {\n label?: ReactNode\n /** Decimal odds. */\n odds: number\n previousOdds?: number\n format?: OddsFormat\n selected?: boolean\n suspended?: boolean\n onSelect?: () => void\n /** Accessible name when the price lengthens. */\n driftedLabel?: string\n shortenedLabel?: string\n}) {\n const drifted = previousOdds !== undefined && previousOdds !== odds\n const up = drifted && odds > previousOdds!\n const down = drifted && odds < previousOdds!\n\n return (\n \n {label && (\n \n {label}\n \n )}\n\n \n {suspended ? '—' : formatOdds(odds, format)}\n {/* Flagged, not animated: a flashing board is unusable. */}\n {!suspended && up && (\n \n )}\n {!suspended && down && (\n \n )}\n \n \n )\n}\n\nexport { OddsDisplay, formatOdds }\n" } ] }