{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"address-display","type":"registry:ui","title":"Address Display","description":"Displays an EVM wallet address with copy-to-clipboard and an optional block explorer link.","docs":"```tsx\nimport { AddressDisplay } from \"@/components/ui/address-display\"\n\n<AddressDisplay address=\"0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045\" />\n```","dependencies":["viem@2.55.19","sonner","lucide-react"],"devDependencies":[],"registryDependencies":["@onchain-ui/sonner"],"files":[{"path":"registry/onchain-ui/address-display.tsx","type":"registry:ui","target":"components/ui/address-display.tsx","content":"\"use client\";\n\nimport { toast } from \"sonner\";\nimport { CopyIcon, ExternalLinkIcon } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { truncateAddress } from \"@/lib/onchain/format\";\nimport { getExplorerUrl } from \"@/lib/onchain/config\";\nimport type { Address } from \"viem\";\nimport type { ReactNode } from \"react\";\n\nexport interface AddressDisplayProps {\n  /** EVM wallet address */\n  address: Address;\n  /** Display label. The copied value is always the underlying address. */\n  label?: ReactNode;\n  /** Applied to the root wrapper */\n  className?: string;\n  /** Applied to the address text or copy trigger */\n  addressClassName?: string;\n  /** Truncate the address. Default: true */\n  truncate?: boolean;\n  /** Number of characters to show at each end when truncated. Default: 4 */\n  truncateChars?: number;\n  /** Show copy-to-clipboard button. Default: true */\n  showCopy?: boolean;\n  /** Show block explorer link. Default: true */\n  showExplorer?: boolean;\n  /** EVM chain id used to pick a known block explorer. Default: 1 (Etherscan) */\n  chainId?: number | null;\n  /** Full explorer URL override e.g. \"https://basescan.org/address/0x...\" */\n  explorerUrl?: string;\n  /** Icon rendered before the address when copy is enabled */\n  copyIcon?: ReactNode;\n  /** Icon rendered for the explorer link */\n  explorerIcon?: ReactNode;\n}\n\nexport function AddressDisplay({\n  address,\n  label,\n  className,\n  addressClassName,\n  truncate = true,\n  truncateChars = 4,\n  showCopy = true,\n  showExplorer = true,\n  chainId,\n  explorerUrl,\n  copyIcon = <CopyIcon className=\"size-3 shrink-0\" />,\n  explorerIcon = <ExternalLinkIcon className=\"size-3 shrink-0\" />,\n}: AddressDisplayProps) {\n  const handleCopy = async () => {\n    try {\n      await navigator.clipboard.writeText(address);\n      toast.success(\"Address copied\");\n    } catch {\n      toast.error(\"Could not copy address\");\n    }\n  };\n\n  // No chain id means \"assume mainnet\", the documented default. A chain id we\n  // do not recognise is different: linking it to Etherscan would point at a\n  // page about a different chain's address space. Drop the link instead and\n  // let `explorerUrl` cover the long tail.\n  const explorerBase = getExplorerUrl(chainId ?? 1);\n  const href =\n    explorerUrl ??\n    (explorerBase ? `${explorerBase}/address/${address}` : null);\n  const displayLabel =\n    label ?? (truncate ? truncateAddress(address, truncateChars) : address);\n  const labelClassName = truncate ? \"whitespace-nowrap\" : \"break-all\";\n\n  return (\n    <div className={cn(\"flex min-w-0 items-center gap-1.5\", className)}>\n      {showCopy ? (\n        <button\n          type=\"button\"\n          onClick={handleCopy}\n          title=\"Copy address\"\n          className={cn(\n            \"flex min-w-0 items-center gap-1.5 font-mono text-xs text-muted-foreground\",\n            \"hover:text-foreground cursor-copy transition-colors duration-150\",\n            addressClassName\n          )}\n        >\n          {copyIcon}\n          <span className={labelClassName}>{displayLabel}</span>\n        </button>\n      ) : (\n        <span\n          className={cn(\n            \"min-w-0 font-mono text-xs text-muted-foreground\",\n            labelClassName,\n            addressClassName\n          )}\n        >\n          {displayLabel}\n        </span>\n      )}\n\n      {showExplorer && href && (\n        <a\n          href={href}\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n          title=\"View on explorer\"\n          className=\"text-muted-foreground hover:text-foreground transition-colors duration-150\"\n        >\n          {explorerIcon}\n        </a>\n      )}\n    </div>\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"},{"path":"lib/onchain/config.ts","type":"registry:lib","target":"lib/onchain/config.ts","content":"import { arbitrum, base, mainnet, optimism, polygon } from \"viem/chains\";\nimport type { AssetGatewayUrls, Chain, PublicClient } from \"viem\";\n\n/** The small client surface identity resolution actually consumes. */\nexport type OnchainReadClient = Pick<\n  PublicClient,\n  \"getEnsAddress\" | \"getEnsAvatar\" | \"getEnsName\" | \"readContract\"\n>;\n\n/**\n * The single place onchain-ui components look for app-wide settings.\n *\n * This file is yours: edit it once and every component follows. There is no\n * provider and no context — components import these functions directly, so\n * they work the same in a server component, a test, or a non-React consumer.\n *\n * Per-instance overrides always win. `<NetworkLogo name=\"…\" />` and\n * `<AddressDisplay explorerUrl=\"…\" />` beat anything configured here.\n */\n\nexport interface OnchainUIConfig {\n  /**\n   * Chains your app supports. Supplies block explorer URLs and display names\n   * for networks that have no built-in entry below.\n   *\n   * Using wagmi? Point this at the config you already wrote:\n   *\n   * ```ts\n   * import { wagmiConfig } from \"@/lib/onchain/wagmi\";\n   * chains: wagmiConfig.chains,\n   * ```\n   */\n  chains: readonly Chain[];\n\n  /**\n   * Returns a viem client for reads that are not tied to a rendered component\n   * — currently ENS and Basename resolution. Leave undefined to use the public\n   * endpoints below.\n   *\n   * Using wagmi? Share its transports instead of configuring RPC twice:\n   *\n   * ```ts\n   * import { getPublicClient } from \"@wagmi/core\";\n   * import { wagmiConfig } from \"@/lib/onchain/wagmi\";\n   * getClient: (chainId) => getPublicClient(wagmiConfig, { chainId }),\n   * ```\n   */\n  getClient?: (chainId: number) => OnchainReadClient | undefined;\n\n  /**\n   * Public gateways used to turn IPFS and Arweave avatar records into URLs.\n   * Replace these with your own gateway in production when reliability or\n   * rate limits matter.\n   */\n  avatarGatewayUrls?: AssetGatewayUrls;\n}\n\nexport const onchainConfig: OnchainUIConfig = {\n  chains: [mainnet, base, arbitrum, optimism, polygon],\n  avatarGatewayUrls: {\n    ipfs: \"https://gateway.pinata.cloud\",\n    arweave: \"https://arweave.net\",\n  },\n};\n\n/**\n * Curated network names and symbols.\n *\n * These are deliberately not derived from the viem chain: `chain.name` gives\n * \"OP Mainnet\" where a badge wants \"Optimism\", and `nativeCurrency.symbol`\n * gives \"ETH\" for Base, Optimism, and Arbitrum alike, which is useless as a\n * network mark. Chains absent here fall back to the configured chain's name.\n */\nconst knownNetworks: Partial<\n  Record<number, { name: string; symbol: string }>\n> = {\n  1: { name: \"Ethereum\", symbol: \"ETH\" },\n  10: { name: \"Optimism\", symbol: \"OP\" },\n  137: { name: \"Polygon\", symbol: \"POL\" },\n  8453: { name: \"Base\", symbol: \"BASE\" },\n  42161: { name: \"Arbitrum\", symbol: \"ARB\" },\n};\n\nfunction findChain(chainId?: number | null) {\n  if (chainId == null) return null;\n  return onchainConfig.chains.find((chain) => chain.id === chainId) ?? null;\n}\n\nexport interface NetworkMeta {\n  name: string | null;\n  symbol: string | null;\n}\n\n/** Display name and symbol for a chain, or nulls when it is unrecognised. */\nexport function getNetworkMeta(chainId?: number | null): NetworkMeta {\n  if (chainId == null) return { name: null, symbol: null };\n\n  const known = knownNetworks[chainId];\n  if (known) return known;\n\n  return { name: findChain(chainId)?.name ?? null, symbol: null };\n}\n\n/**\n * Block explorer base URL for a chain, or null when we have none.\n *\n * Derived from the configured chains rather than a second hardcoded map —\n * viem's chain objects already carry `blockExplorers`. Null is a real answer:\n * an address on an unrecognised chain does not exist in mainnet's address\n * space, so callers render no link rather than a misleading one.\n */\nexport function getExplorerUrl(chainId?: number | null): string | null {\n  const chain = findChain(chainId);\n  return chain?.blockExplorers?.default.url ?? null;\n}\n\n/** A read client for `chainId`, if the app configured one. */\nexport function getConfiguredClient(\n  chainId: number\n): OnchainReadClient | undefined {\n  return onchainConfig.getClient?.(chainId);\n}\n\n/** App-wide gateways for ENS and Basename avatar assets, if configured. */\nexport function getConfiguredAvatarGatewayUrls(): AssetGatewayUrls | undefined {\n  return onchainConfig.avatarGatewayUrls;\n}\n"}]}