{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"token-price","type":"registry:ui","title":"Token Price","description":"Formats token prices with optional positive, negative, or neutral change.","docs":"```tsx\nimport { TokenPrice } from \"@/components/ui/token-price\"\n\n<TokenPrice value={2456.78} change={2.34} />\n```","dependencies":["lucide-react"],"devDependencies":[],"registryDependencies":[],"files":[{"path":"registry/onchain-ui/token-price.tsx","type":"registry:ui","target":"components/ui/token-price.tsx","content":"import { ArrowDownIcon, ArrowUpIcon, MinusIcon } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  parseNumericValue,\n  type NumericValue,\n} from \"@/lib/onchain/format\";\nimport type { ReactNode } from \"react\";\n\ntype PriceValue = NumericValue;\ntype Trend = \"down\" | \"neutral\" | \"up\";\n\nconst trendClasses = {\n  down: \"text-red-600 dark:text-red-400\",\n  neutral: \"text-muted-foreground\",\n  up: \"text-emerald-600 dark:text-emerald-400\",\n} as const;\n\nexport interface TokenPriceProps {\n  /** Token price value */\n  value: PriceValue;\n  /** ISO 4217 currency code. Default: USD */\n  currency?: string;\n  /** Locale passed to Intl.NumberFormat */\n  locale?: string;\n  /** Use compact notation, e.g. $3.2K */\n  compact?: boolean;\n  /** Value shown when price is null, undefined, empty, or invalid. Default: -- */\n  fallback?: ReactNode;\n  /** Minimum fraction digits for the price */\n  minimumFractionDigits?: number;\n  /** Maximum fraction digits for the price */\n  maximumFractionDigits?: number;\n  /** 24h-style percentage change, e.g. -2.34 means -2.34% */\n  change?: number | null;\n  /** Override the derived change direction */\n  trend?: Trend;\n  /** Show trend icon before the change. Default: true */\n  showChangeIcon?: boolean;\n  /** Fraction digits for the change value. Default: 2 */\n  changeFractionDigits?: number;\n  /** Hide the plus sign for positive changes. Default: false */\n  hidePositiveSign?: boolean;\n  /** Applied to the root wrapper */\n  className?: string;\n  /** Applied to the formatted price */\n  priceClassName?: string;\n  /** Applied to the change badge */\n  changeClassName?: string;\n}\n\nfunction getMaximumFractionDigits(value: number) {\n  const abs = Math.abs(value);\n  if (abs >= 1) return 2;\n  if (abs >= 0.01) return 4;\n  return 6;\n}\n\nfunction formatPrice({\n  compact,\n  currency,\n  locale,\n  maximumFractionDigits,\n  minimumFractionDigits,\n  value,\n}: {\n  compact: boolean;\n  currency: string;\n  locale?: string;\n  maximumFractionDigits?: number;\n  minimumFractionDigits?: number;\n  value: number;\n}) {\n  const abs = Math.abs(value);\n  const hasExplicitDigits =\n    maximumFractionDigits !== undefined || minimumFractionDigits !== undefined;\n\n  // Sub-cent prices round to $0.00 with fraction digits; keep the leading\n  // significant figures instead, e.g. $0.00000041.\n  if (!hasExplicitDigits && abs > 0 && abs < 0.01) {\n    return new Intl.NumberFormat(locale, {\n      style: \"currency\",\n      currency,\n      currencyDisplay: \"narrowSymbol\",\n      notation: compact ? \"compact\" : \"standard\",\n      maximumSignificantDigits: 2,\n    }).format(value);\n  }\n\n  const resolvedMaximumFractionDigits =\n    maximumFractionDigits ?? getMaximumFractionDigits(value);\n\n  return new Intl.NumberFormat(locale, {\n    style: \"currency\",\n    currency,\n    currencyDisplay: \"narrowSymbol\",\n    notation: compact ? \"compact\" : \"standard\",\n    minimumFractionDigits,\n    // Intl.NumberFormat throws a RangeError when min > max\n    maximumFractionDigits: Math.max(\n      resolvedMaximumFractionDigits,\n      minimumFractionDigits ?? 0\n    ),\n  }).format(value);\n}\n\nfunction getTrend(change?: number | null): Trend {\n  if (!change) return \"neutral\";\n  return change > 0 ? \"up\" : \"down\";\n}\n\nfunction formatChange(\n  change: number,\n  fractionDigits: number,\n  hidePositiveSign: boolean\n) {\n  const sign = change > 0 && !hidePositiveSign ? \"+\" : \"\";\n  return `${sign}${change.toFixed(fractionDigits)}%`;\n}\n\nfunction TrendIcon({ trend }: { trend: Trend }) {\n  if (trend === \"up\") return <ArrowUpIcon className=\"size-3\" />;\n  if (trend === \"down\") return <ArrowDownIcon className=\"size-3\" />;\n  return <MinusIcon className=\"size-3\" />;\n}\n\nexport function TokenPrice({\n  value,\n  currency = \"USD\",\n  locale,\n  compact = false,\n  fallback = \"--\",\n  minimumFractionDigits,\n  maximumFractionDigits,\n  change,\n  trend,\n  showChangeIcon = true,\n  changeFractionDigits = 2,\n  hidePositiveSign = false,\n  className,\n  priceClassName,\n  changeClassName,\n}: TokenPriceProps) {\n  const parsedValue = parseNumericValue(value);\n  const shouldShowChange = typeof change === \"number\" && Number.isFinite(change);\n  const derivedTrend = trend ?? getTrend(change);\n\n  return (\n    <span className={cn(\"inline-flex items-center gap-2\", className)}>\n      <span className={cn(\"font-mono tabular-nums\", priceClassName)}>\n        {parsedValue === null\n          ? fallback\n          : formatPrice({\n              compact,\n              currency,\n              locale,\n              maximumFractionDigits,\n              minimumFractionDigits,\n              value: parsedValue,\n            })}\n      </span>\n\n      {shouldShowChange && (\n        <span\n          className={cn(\n            \"inline-flex items-center gap-0.5 font-mono text-xs tabular-nums\",\n            trendClasses[derivedTrend],\n            changeClassName\n          )}\n        >\n          {showChangeIcon && <TrendIcon trend={derivedTrend} />}\n          {formatChange(change, changeFractionDigits, hidePositiveSign)}\n        </span>\n      )}\n    </span>\n  );\n}\n"},{"path":"lib/onchain/format.ts","type":"registry:lib","target":"lib/onchain/format.ts","content":"export function truncateAddress(address: string, chars = 4): string {\n  if (!address) return \"\";\n\n  return `${address.slice(0, chars + 2)}...${address.slice(-chars)}`;\n}\n\nexport type NumericValue = number | string | null | undefined;\n\n/** Parses numbers and numeric strings; anything else becomes null. */\nexport function parseNumericValue(value: NumericValue): number | null {\n  if (typeof value === \"number\") return Number.isFinite(value) ? value : null;\n  if (typeof value === \"string\" && value.trim() !== \"\") {\n    const parsed = Number(value);\n    return Number.isFinite(parsed) ? parsed : null;\n  }\n  return null;\n}\n\n"}]}