{"slug":"swap","name":"Multi-chain Swap","description":"Cross-chain swap widget with chain + token selectors, morphing views, animated flip and quote.","category":"motion","source_url":"https://beui.saura3h.xyz/r/swap/raw","detail_url":"https://beui.saura3h.xyz/r/swap","raw_url":"https://beui.saura3h.xyz/r/swap/raw","page_url":"https://beui.saura3h.xyz/components/motion/swap","dependencies":["lucide-react","motion","react"],"internal":["@/components/motion/swap","@/lib/utils"],"files":[{"path":"components/motion/swap.tsx","type":"component","content":"\"use client\";\n\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport {\n  ArrowDownUp,\n  Check,\n  ChevronDown,\n  Loader2,\n  Search,\n  Send,\n  Settings,\n  Wallet,\n  X,\n} from \"lucide-react\";\nimport { useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\n/* ============================================================\n * Types + mock data\n * ============================================================ */\n\nexport type Chain = { id: string; name: string; color: string; symbol: string };\nexport type Token = {\n  id: string;\n  symbol: string;\n  name: string;\n  chainId: string;\n  address?: string;\n  balance?: number;\n  usd?: number;\n  trending?: boolean;\n  popular?: boolean;\n};\n\nconst CHAINS: Chain[] = [\n  { id: \"eth\", name: \"Ethereum\", color: \"#627EEA\", symbol: \"Ξ\" },\n  { id: \"sol\", name: \"Solana\", color: \"#9945FF\", symbol: \"◎\" },\n  { id: \"base\", name: \"Base\", color: \"#0052FF\", symbol: \"B\" },\n  { id: \"arb\", name: \"Arbitrum\", color: \"#28A0F0\", symbol: \"A\" },\n  { id: \"op\", name: \"Optimism\", color: \"#FF0420\", symbol: \"O\" },\n  { id: \"poly\", name: \"Polygon\", color: \"#8247E5\", symbol: \"P\" },\n  { id: \"bnb\", name: \"BNB\", color: \"#F0B90B\", symbol: \"B\" },\n  { id: \"avax\", name: \"Avalanche\", color: \"#E84142\", symbol: \"A\" },\n];\n\nconst TOKENS: Token[] = [\n  {\n    id: \"eth-eth\",\n    symbol: \"ETH\",\n    name: \"Ether\",\n    chainId: \"eth\",\n    balance: 1.245,\n    usd: 3142,\n    popular: true,\n  },\n  {\n    id: \"eth-weth\",\n    symbol: \"WETH\",\n    name: \"Wrapped Ether\",\n    chainId: \"eth\",\n    balance: 0.5,\n    usd: 3142,\n    popular: true,\n  },\n  {\n    id: \"eth-usdc\",\n    symbol: \"USDC\",\n    name: \"USD Coin\",\n    chainId: \"eth\",\n    balance: 4521,\n    usd: 1,\n    popular: true,\n  },\n  {\n    id: \"eth-usdt\",\n    symbol: \"USDT\",\n    name: \"Tether\",\n    chainId: \"eth\",\n    balance: 2100,\n    usd: 1,\n    popular: true,\n  },\n  {\n    id: \"eth-dai\",\n    symbol: \"DAI\",\n    name: \"Dai Stablecoin\",\n    chainId: \"eth\",\n    balance: 800,\n    usd: 1,\n    popular: true,\n  },\n  {\n    id: \"eth-wbtc\",\n    symbol: \"WBTC\",\n    name: \"Wrapped Bitcoin\",\n    chainId: \"eth\",\n    balance: 0.04,\n    usd: 96000,\n    popular: true,\n  },\n  {\n    id: \"eth-link\",\n    symbol: \"LINK\",\n    name: \"ChainLink Token\",\n    chainId: \"eth\",\n    balance: 120,\n    usd: 22,\n    address: \"0x51...86ca\",\n    trending: true,\n  },\n  {\n    id: \"eth-aztec\",\n    symbol: \"AZTEC\",\n    name: \"AZTEC\",\n    chainId: \"eth\",\n    balance: 0,\n    address: \"0xa2...62d2\",\n    trending: true,\n  },\n  {\n    id: \"eth-cfg\",\n    symbol: \"CFG\",\n    name: \"Centrifuge\",\n    chainId: \"eth\",\n    balance: 0,\n    address: \"0xcc...8a94\",\n    trending: true,\n  },\n  {\n    id: \"eth-ondo\",\n    symbol: \"ONDO\",\n    name: \"Ondo\",\n    chainId: \"eth\",\n    balance: 0,\n    address: \"0xfa...9be3\",\n    trending: true,\n  },\n  {\n    id: \"sol-sol\",\n    symbol: \"SOL\",\n    name: \"Solana\",\n    chainId: \"sol\",\n    balance: 12.4,\n    usd: 168,\n  },\n  {\n    id: \"sol-usdc\",\n    symbol: \"USDC\",\n    name: \"USD Coin\",\n    chainId: \"sol\",\n    balance: 985.32,\n    usd: 1,\n  },\n  {\n    id: \"base-eth\",\n    symbol: \"ETH\",\n    name: \"Ether\",\n    chainId: \"base\",\n    balance: 0.65,\n    usd: 3142,\n  },\n  {\n    id: \"base-usdc\",\n    symbol: \"USDC\",\n    name: \"USD Coin\",\n    chainId: \"base\",\n    balance: 1200,\n    usd: 1,\n  },\n  {\n    id: \"arb-arb\",\n    symbol: \"ARB\",\n    name: \"Arbitrum\",\n    chainId: \"arb\",\n    balance: 800,\n    usd: 0.95,\n  },\n  {\n    id: \"arb-eth\",\n    symbol: \"ETH\",\n    name: \"Ether\",\n    chainId: \"arb\",\n    balance: 0.32,\n    usd: 3142,\n  },\n  {\n    id: \"op-op\",\n    symbol: \"OP\",\n    name: \"Optimism\",\n    chainId: \"op\",\n    balance: 450,\n    usd: 2.8,\n  },\n  {\n    id: \"poly-matic\",\n    symbol: \"MATIC\",\n    name: \"Polygon\",\n    chainId: \"poly\",\n    balance: 1500,\n    usd: 0.75,\n  },\n  {\n    id: \"bnb-bnb\",\n    symbol: \"BNB\",\n    name: \"BNB\",\n    chainId: \"bnb\",\n    balance: 0,\n    usd: 620,\n  },\n  {\n    id: \"avax-avax\",\n    symbol: \"AVAX\",\n    name: \"Avalanche\",\n    chainId: \"avax\",\n    balance: 0,\n    usd: 38,\n  },\n];\n\nconst EASE = [0.16, 1, 0.3, 1] as const;\nconst DRAWER_EASE = [0.32, 0.72, 0, 1] as const;\n\n/* ============================================================\n * MultiChainSwap\n * ============================================================ */\n\nexport interface MultiChainSwapProps {\n  chains?: Chain[];\n  tokens?: Token[];\n  defaultFromId?: string;\n  defaultToId?: string;\n  className?: string;\n}\n\nexport function MultiChainSwap({\n  chains = CHAINS,\n  tokens = TOKENS,\n  defaultFromId = \"eth-eth\",\n  defaultToId = \"sol-sol\",\n  className,\n}: MultiChainSwapProps) {\n  const reduce = useReducedMotion();\n  const [fromId, setFromId] = useState(defaultFromId);\n  const [toId, setToId] = useState(defaultToId);\n  const [amount, setAmount] = useState(\"1\");\n  const [flipRot, setFlipRot] = useState(0);\n  const [quoting, setQuoting] = useState(false);\n  const [picking, setPicking] = useState<\"from\" | \"to\" | null>(null);\n  const [showDest, setShowDest] = useState(false);\n  const [destAddress, setDestAddress] = useState(\"\");\n\n  const from = tokens.find((t) => t.id === fromId)!;\n  const to = tokens.find((t) => t.id === toId)!;\n  const fromChain = chains.find((c) => c.id === from.chainId)!;\n  const toChain = chains.find((c) => c.id === to.chainId)!;\n\n  const numericAmount = Number(amount) || 0;\n  const rate = useMemo(() => {\n    if (!from.usd || !to.usd) return 1;\n    return from.usd / to.usd;\n  }, [from.usd, to.usd]);\n  const toAmount = numericAmount * rate;\n  const networkFee = 0.42;\n  const eta = \"≈ 24s\";\n\n  useEffect(() => {\n    if (numericAmount === 0) return;\n    setQuoting(true);\n    const id = setTimeout(() => setQuoting(false), 450);\n    return () => clearTimeout(id);\n  }, [numericAmount, fromId, toId]);\n\n  const flip = () => {\n    setFlipRot((r) => r + 180);\n    setFromId(toId);\n    setToId(fromId);\n  };\n\n  const pickToken = (id: string) => {\n    if (!picking) return;\n    if (picking === \"from\") {\n      if (id === toId) setToId(fromId);\n      setFromId(id);\n    } else {\n      if (id === fromId) setFromId(toId);\n      setToId(id);\n    }\n    setPicking(null);\n  };\n\n  return (\n    <div\n      className={cn(\n        \"relative isolate w-full max-w-[420px] overflow-hidden rounded-3xl\",\n        \"border border-(--color-border-strong)/20 bg-(--color-bg-elev)\",\n        className,\n      )}\n    >\n      {/* Header */}\n      <div className=\"flex h-12 items-center justify-between border-b border-(--color-border)/50 px-3\">\n        <span className=\"px-2 text-sm font-semibold tracking-tight text-(--color-fg)\">\n          Swap\n        </span>\n        <button\n          type=\"button\"\n          aria-label=\"Settings\"\n          className=\"inline-flex h-8 w-8 items-center justify-center rounded-full text-(--color-fg-muted) hover:bg-(--color-fg)/5 hover:text-(--color-fg) press\"\n        >\n          <Settings className=\"h-4 w-4\" />\n        </button>\n      </div>\n\n      {/* Swap form (always mounted, fixed height) */}\n      <div className=\"flex flex-col gap-1.5 p-4\">\n        <Field\n          side=\"from\"\n          token={from}\n          chain={fromChain}\n          amount={amount}\n          onAmount={setAmount}\n          editable\n          quoting={false}\n          onOpenPicker={() => setPicking(\"from\")}\n        />\n\n        <FlipButton rotation={flipRot} reduce={!!reduce} onClick={flip} />\n\n        <Field\n          side=\"to\"\n          token={to}\n          chain={toChain}\n          amount={toAmount > 0 ? formatAmount(toAmount) : \"\"}\n          editable={false}\n          quoting={quoting}\n          onOpenPicker={() => setPicking(\"to\")}\n        />\n\n        <QuoteRow\n          from={from}\n          to={to}\n          rate={rate}\n          fee={networkFee}\n          slippage={0.5}\n          eta={eta}\n          quoting={quoting}\n        />\n\n        <DestinationRow\n          show={showDest}\n          onToggle={() => {\n            if (showDest) setDestAddress(\"\");\n            setShowDest((v) => !v);\n          }}\n          address={destAddress}\n          onAddress={setDestAddress}\n          reduce={!!reduce}\n        />\n\n        <ActionButton\n          from={from}\n          to={to}\n          amount={numericAmount}\n          destAddress={destAddress}\n        />\n      </div>\n\n      {/* Token picker — bottom sheet anchored to this card */}\n      <TokenPicker\n        open={picking !== null}\n        side={picking}\n        chains={chains}\n        tokens={tokens}\n        selectedId={picking === \"from\" ? fromId : toId}\n        onPick={pickToken}\n        onClose={() => setPicking(null)}\n        reduce={!!reduce}\n      />\n    </div>\n  );\n}\n\n/* ============================================================\n * Field\n * ============================================================ */\n\nfunction Field({\n  side,\n  token,\n  chain,\n  amount,\n  onAmount,\n  editable,\n  quoting,\n  onOpenPicker,\n}: {\n  side: \"from\" | \"to\";\n  token: Token;\n  chain: Chain;\n  amount: string;\n  onAmount?: (v: string) => void;\n  editable: boolean;\n  quoting: boolean;\n  onOpenPicker: () => void;\n}) {\n  const id = useId();\n  const usdValue = (Number(amount) || 0) * (token.usd ?? 0);\n  return (\n    <div className=\"relative rounded-2xl border border-(--color-border)/50 bg-(--color-bg)/40 p-3.5\">\n      <label\n        htmlFor={id}\n        className=\"mb-2 block text-[11px] font-medium uppercase tracking-wider text-(--color-fg-muted)\"\n      >\n        {side === \"from\" ? \"You pay\" : \"You get\"}\n      </label>\n\n      <div className=\"flex items-center justify-between gap-3\">\n        <div className=\"min-w-0 flex-1\">\n          {editable ? (\n            <input\n              id={id}\n              inputMode=\"decimal\"\n              value={amount}\n              onChange={(e) => onAmount?.(sanitizeAmount(e.target.value))}\n              placeholder=\"0\"\n              className=\"w-full bg-transparent text-2xl font-semibold tracking-tight text-(--color-fg) tabular-nums outline-none placeholder:text-(--color-fg-muted)/60\"\n            />\n          ) : (\n            <div className=\"flex h-9 items-center gap-2 text-2xl font-semibold tracking-tight tabular-nums\">\n              <motion.span\n                animate={{\n                  opacity: quoting ? 0.55 : 1,\n                  filter: quoting ? \"blur(2px)\" : \"blur(0px)\",\n                }}\n                transition={{ duration: 0.18, ease: EASE }}\n                className=\"text-(--color-fg)\"\n              >\n                {amount || \"0\"}\n              </motion.span>\n              <AnimatePresence>\n                {quoting ? (\n                  <motion.span\n                    initial={{ opacity: 0, scale: 0.85 }}\n                    animate={{ opacity: 1, scale: 1 }}\n                    exit={{ opacity: 0, scale: 0.85 }}\n                    transition={{ duration: 0.14, ease: EASE }}\n                  >\n                    <Loader2 className=\"h-4 w-4 animate-spin text-(--color-fg-muted)\" />\n                  </motion.span>\n                ) : null}\n              </AnimatePresence>\n            </div>\n          )}\n          <p className=\"mt-1 text-[11px] text-(--color-fg-muted) tabular-nums\">\n            ≈ ${formatAmount(usdValue, 2)}\n          </p>\n        </div>\n\n        <button\n          type=\"button\"\n          onClick={onOpenPicker}\n          className=\"group inline-flex h-10 items-center gap-2 rounded-full border border-(--color-border) bg-(--color-bg-elev) pl-1 pr-2.5 text-sm font-semibold text-(--color-fg) press hover:border-(--color-border-strong)\"\n        >\n          <TokenDot token={token} chain={chain} />\n          <span>{token.symbol}</span>\n          <ChevronDown className=\"h-3.5 w-3.5 text-(--color-fg-muted)\" />\n        </button>\n      </div>\n\n      <div className=\"mt-2 flex items-center justify-between text-[11px] text-(--color-fg-muted)\">\n        <span className=\"inline-flex items-center gap-1\">\n          <Wallet className=\"h-3 w-3\" />\n          <span className=\"tabular-nums\">\n            {token.balance ? formatAmount(token.balance) : \"0.00\"}\n          </span>\n          <span>· {chain.name}</span>\n        </span>\n        {side === \"from\" && token.balance ? (\n          <button\n            type=\"button\"\n            onClick={() => onAmount?.(String(token.balance))}\n            className=\"rounded px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-(--color-fg-muted) hover:bg-(--color-fg)/5 hover:text-(--color-fg)\"\n          >\n            Max\n          </button>\n        ) : null}\n      </div>\n    </div>\n  );\n}\n\n/* ============================================================\n * Flip button\n * ============================================================ */\n\nfunction FlipButton({\n  rotation,\n  reduce,\n  onClick,\n}: {\n  rotation: number;\n  reduce: boolean;\n  onClick: () => void;\n}) {\n  return (\n    <div className=\"relative -my-4 flex justify-center\" style={{ zIndex: 1 }}>\n      <motion.button\n        type=\"button\"\n        onClick={onClick}\n        aria-label=\"Reverse direction\"\n        whileTap={reduce ? undefined : { scale: 0.9 }}\n        animate={reduce ? undefined : { rotate: rotation }}\n        transition={{ type: \"spring\", stiffness: 380, damping: 26, mass: 0.6 }}\n        className=\"inline-flex h-9 w-9 items-center justify-center rounded-full border-[3px] border-(--color-bg-elev) bg-(--color-fg)/10 text-(--color-fg) backdrop-blur\"\n      >\n        <ArrowDownUp className=\"h-3.5 w-3.5\" />\n      </motion.button>\n    </div>\n  );\n}\n\n/* ============================================================\n * Quote row\n * ============================================================ */\n\nfunction QuoteRow({\n  from,\n  to,\n  rate,\n  fee,\n  slippage,\n  eta,\n  quoting,\n}: {\n  from: Token;\n  to: Token;\n  rate: number;\n  fee: number;\n  slippage: number;\n  eta: string;\n  quoting: boolean;\n}) {\n  return (\n    <div className=\"mt-3 grid grid-cols-2 gap-x-4 gap-y-1.5 rounded-xl border border-(--color-border)/50 bg-(--color-bg)/40 px-3.5 py-2.5 text-[11px]\">\n      <span className=\"text-(--color-fg-muted)\">Rate</span>\n      <span className=\"text-right tabular-nums text-(--color-fg)\">\n        {quoting ? (\n          <Loader2 className=\"ml-auto inline h-3 w-3 animate-spin text-(--color-fg-muted)\" />\n        ) : (\n          <>\n            1 {from.symbol} ≈ {formatAmount(rate)} {to.symbol}\n          </>\n        )}\n      </span>\n      <span className=\"text-(--color-fg-muted)\">Network fee</span>\n      <span className=\"text-right tabular-nums text-(--color-fg)\">\n        ${fee.toFixed(2)}\n      </span>\n      <span className=\"text-(--color-fg-muted)\">Slippage</span>\n      <span className=\"text-right tabular-nums text-(--color-fg)\">\n        {slippage.toFixed(2)}%\n      </span>\n      <span className=\"text-(--color-fg-muted)\">ETA</span>\n      <span className=\"text-right text-(--color-fg)\">{eta}</span>\n    </div>\n  );\n}\n\n/* ============================================================\n * Action button\n * ============================================================ */\n\nfunction ActionButton({\n  from,\n  to,\n  amount,\n  destAddress,\n}: {\n  from: Token;\n  to: Token;\n  amount: number;\n  destAddress: string;\n}) {\n  const noAmount = amount <= 0;\n  const overBalance = from.balance !== undefined && amount > from.balance;\n  const validDest = destAddress && isValidAddress(destAddress);\n  const label = noAmount\n    ? \"Enter an amount\"\n    : overBalance\n      ? `Insufficient ${from.symbol}`\n      : validDest\n        ? `Swap + Send to ${truncateAddress(destAddress)}`\n        : `Swap ${from.symbol} → ${to.symbol}`;\n  const disabled = noAmount || overBalance;\n\n  return (\n    <motion.button\n      type=\"button\"\n      whileTap={disabled ? undefined : { scale: 0.97 }}\n      transition={{ type: \"spring\", stiffness: 500, damping: 30, mass: 0.6 }}\n      disabled={disabled}\n      className={cn(\n        \"mt-3 inline-flex h-12 w-full items-center justify-center rounded-2xl text-sm font-semibold transition-colors\",\n        disabled\n          ? \"cursor-not-allowed bg-(--color-fg)/10 text-(--color-fg-muted)\"\n          : \"bg-(--color-fg) text-(--color-bg) hover:bg-(--color-fg)/90\",\n      )}\n    >\n      <AnimatePresence mode=\"wait\" initial={false}>\n        <motion.span\n          key={label}\n          initial={{ opacity: 0 }}\n          animate={{ opacity: 1 }}\n          exit={{ opacity: 0 }}\n          transition={{ duration: 0.14, ease: EASE }}\n        >\n          {label}\n        </motion.span>\n      </AnimatePresence>\n    </motion.button>\n  );\n}\n\n/* ============================================================\n * Destination address row\n * ============================================================ */\n\nfunction DestinationRow({\n  show,\n  onToggle,\n  address,\n  onAddress,\n  reduce,\n}: {\n  show: boolean;\n  onToggle: () => void;\n  address: string;\n  onAddress: (v: string) => void;\n  reduce: boolean;\n}) {\n  const inputRef = useRef<HTMLInputElement>(null);\n  const hasAddress = address.length > 0;\n  const valid = isValidAddress(address);\n\n  useEffect(() => {\n    if (!show) return;\n    requestAnimationFrame(() =>\n      inputRef.current?.focus({ preventScroll: true }),\n    );\n  }, [show]);\n\n  return (\n    <div className=\"mt-1 overflow-hidden rounded-xl border border-(--color-border)/50 bg-(--color-bg)/40\">\n      <button\n        type=\"button\"\n        onClick={onToggle}\n        className=\"flex w-full items-center justify-between gap-2 px-3.5 py-2.5 text-[12px] text-(--color-fg-muted) hover:text-(--color-fg)\"\n      >\n        <span className=\"flex items-center gap-2\">\n          <Send className=\"h-3.5 w-3.5 shrink-0\" />\n          <span>\n            {show && hasAddress && valid\n              ? `To: ${truncateAddress(address)}`\n              : \"Send to different address\"}\n          </span>\n        </span>\n        <motion.span\n          animate={reduce ? {} : { rotate: show ? 180 : 0 }}\n          transition={{ duration: 0.2, ease: EASE }}\n          style={{ display: \"inline-flex\" }}\n        >\n          <ChevronDown className=\"h-3.5 w-3.5\" />\n        </motion.span>\n      </button>\n\n      <AnimatePresence>\n        {show ? (\n          <motion.div\n            key=\"dest-input\"\n            initial={reduce ? { opacity: 0 } : { height: 0, opacity: 0 }}\n            animate={reduce ? { opacity: 1 } : { height: \"auto\", opacity: 1 }}\n            exit={reduce ? { opacity: 0 } : { height: 0, opacity: 0 }}\n            transition={{ duration: 0.22, ease: EASE }}\n            style={{ overflow: \"hidden\" }}\n          >\n            <div className=\"border-t border-(--color-border)/50 px-3.5 pb-3 pt-2.5\">\n              <div\n                className={cn(\n                  \"flex items-center gap-2 rounded-lg border px-2.5 py-2 transition-colors\",\n                  hasAddress && !valid\n                    ? \"border-red-500/40\"\n                    : \"border-(--color-border)\",\n                )}\n              >\n                <input\n                  ref={inputRef}\n                  value={address}\n                  onChange={(e) => onAddress(e.target.value.trim())}\n                  placeholder=\"0x... or name.eth\"\n                  spellCheck={false}\n                  className=\"min-w-0 flex-1 bg-transparent font-mono text-[12px] text-(--color-fg) outline-none placeholder:text-(--color-fg-muted)/60\"\n                />\n                <AnimatePresence mode=\"wait\">\n                  {hasAddress ? (\n                    valid ? (\n                      <motion.span\n                        key=\"check\"\n                        initial={{ opacity: 0, scale: 0.7 }}\n                        animate={{ opacity: 1, scale: 1 }}\n                        exit={{ opacity: 0, scale: 0.7 }}\n                        transition={{ duration: 0.14, ease: EASE }}\n                      >\n                        <Check className=\"h-3.5 w-3.5 shrink-0 text-green-500\" />\n                      </motion.span>\n                    ) : (\n                      <motion.button\n                        key=\"clear\"\n                        type=\"button\"\n                        onClick={() => onAddress(\"\")}\n                        aria-label=\"Clear address\"\n                        initial={{ opacity: 0, scale: 0.7 }}\n                        animate={{ opacity: 1, scale: 1 }}\n                        exit={{ opacity: 0, scale: 0.7 }}\n                        transition={{ duration: 0.14, ease: EASE }}\n                        className=\"shrink-0 text-(--color-fg-muted) hover:text-(--color-fg)\"\n                      >\n                        <X className=\"h-3.5 w-3.5\" />\n                      </motion.button>\n                    )\n                  ) : null}\n                </AnimatePresence>\n              </div>\n            </div>\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  );\n}\n\n/* ============================================================\n * TokenPicker — bottom sheet anchored to swap card\n * ============================================================ */\n\nfunction TokenPicker({\n  open,\n  side,\n  chains,\n  tokens,\n  selectedId,\n  onPick,\n  onClose,\n  reduce,\n}: {\n  open: boolean;\n  side: \"from\" | \"to\" | null;\n  chains: Chain[];\n  tokens: Token[];\n  selectedId: string;\n  onPick: (id: string) => void;\n  onClose: () => void;\n  reduce: boolean;\n}) {\n  const [chainFilter, setChainFilter] = useState<string>(\"all\"); // \"all\" or chain.id\n  const [q, setQ] = useState(\"\");\n  const inputRef = useRef<HTMLInputElement>(null);\n\n  useEffect(() => {\n    if (!open) return;\n    setQ(\"\");\n    // Focus without letting the browser scroll the page to bring the input\n    // into view — that's what causes the swap form behind to \"shift up\".\n    requestAnimationFrame(() =>\n      inputRef.current?.focus({ preventScroll: true }),\n    );\n  }, [open]);\n\n  // Esc to close\n  useEffect(() => {\n    if (!open) return;\n    const onKey = (e: KeyboardEvent) => {\n      if (e.key === \"Escape\") onClose();\n    };\n    window.addEventListener(\"keydown\", onKey);\n    return () => window.removeEventListener(\"keydown\", onKey);\n  }, [open, onClose]);\n\n  const popular = useMemo(\n    () => tokens.filter((t) => t.popular).slice(0, 6),\n    [tokens],\n  );\n\n  const filtered = useMemo(() => {\n    const needle = q.trim().toLowerCase();\n    return tokens.filter((t) => {\n      if (chainFilter !== \"all\" && t.chainId !== chainFilter) return false;\n      if (!needle) return true;\n      const chain = chains.find((c) => c.id === t.chainId);\n      return [t.symbol, t.name, chain?.name, t.address].some((h) =>\n        h?.toLowerCase().includes(needle),\n      );\n    });\n  }, [q, chainFilter, tokens, chains]);\n\n  return (\n    <AnimatePresence>\n      {open ? (\n        <>\n          {/* Backdrop inside the card */}\n          <motion.button\n            key=\"backdrop\"\n            type=\"button\"\n            aria-label=\"Close\"\n            onClick={onClose}\n            initial={{ opacity: 0 }}\n            animate={{ opacity: 1 }}\n            exit={{ opacity: 0 }}\n            transition={{ duration: 0.2, ease: EASE }}\n            className=\"absolute inset-0 z-10 cursor-default bg-(--color-bg)/40 backdrop-blur-sm\"\n          />\n\n          {/* Sheet panel */}\n          <motion.div\n            key=\"sheet\"\n            initial={reduce ? { opacity: 0 } : { opacity: 0, y: \"100%\" }}\n            animate={reduce ? { opacity: 1 } : { opacity: 1, y: 0 }}\n            exit={reduce ? { opacity: 0 } : { opacity: 0, y: \"100%\" }}\n            transition={\n              reduce\n                ? { duration: 0.18, ease: EASE }\n                : { type: \"spring\", stiffness: 420, damping: 40, mass: 0.5 }\n            }\n            className=\"absolute inset-x-0 bottom-0 z-20 flex max-h-[92%] flex-col rounded-t-3xl border-t border-(--color-border-strong) bg-(--color-bg-elev) shadow-[0_-20px_40px_-20px_rgb(0_0_0/0.4)]\"\n            role=\"dialog\"\n            aria-modal=\"true\"\n            aria-label={`Select ${side === \"from\" ? \"from\" : \"to\"} token`}\n          >\n            {/* Drag handle (visual only) */}\n            <div className=\"flex justify-center pt-2.5 pb-1\">\n              <span className=\"h-1 w-9 rounded-full bg-(--color-fg)/15\" />\n            </div>\n\n            {/* Search */}\n            <div className=\"flex items-center gap-2 border-b border-(--color-border) px-4 pb-3\">\n              <Search className=\"h-4 w-4 shrink-0 text-(--color-fg-muted)\" />\n              <input\n                ref={inputRef}\n                value={q}\n                onChange={(e) => setQ(e.target.value)}\n                placeholder=\"Search name or paste address\"\n                className=\"w-full bg-transparent text-sm text-(--color-fg) outline-none placeholder:text-(--color-fg-muted)/70\"\n              />\n              <button\n                type=\"button\"\n                onClick={onClose}\n                aria-label=\"Close\"\n                className=\"inline-flex h-7 w-7 items-center justify-center rounded-full text-(--color-fg-muted) hover:bg-(--color-fg)/5 hover:text-(--color-fg)\"\n              >\n                <X className=\"h-3.5 w-3.5\" />\n              </button>\n            </div>\n\n            {/* Chain chips */}\n            <div className=\"scrollbar-hide flex items-center gap-1.5 overflow-x-auto border-b border-(--color-border) px-3 py-5\">\n              <ChainChip\n                active={chainFilter === \"all\"}\n                onClick={() => setChainFilter(\"all\")}\n                label=\"All\"\n              />\n              {chains.map((c) => (\n                <ChainChip\n                  key={c.id}\n                  active={chainFilter === c.id}\n                  onClick={() => setChainFilter(c.id)}\n                  chain={c}\n                />\n              ))}\n            </div>\n\n            {/* Scroll area */}\n            <div className=\"flex-1 overflow-y-auto px-3 pb-4 pt-3\">\n              {/* Popular */}\n              {!q && chainFilter === \"all\" ? (\n                <>\n                  <p className=\"px-1 pb-2 text-[11px] font-semibold uppercase tracking-wider text-(--color-fg-muted)\">\n                    Most popular\n                  </p>\n                  <div className=\"scrollbar-hide flex items-center gap-1.5 overflow-x-auto pb-3\">\n                    {popular.map((t) => {\n                      const chain = chains.find((c) => c.id === t.chainId)!;\n                      return (\n                        <button\n                          key={t.id}\n                          type=\"button\"\n                          onClick={() => onPick(t.id)}\n                          className=\"inline-flex shrink-0 items-center gap-2 rounded-full border border-(--color-border) bg-(--color-bg)/50 py-1 pl-1 pr-3 text-sm font-semibold text-(--color-fg) press hover:border-(--color-border-strong)\"\n                        >\n                          <TokenDot token={t} chain={chain} size={22} />\n                          {t.symbol}\n                        </button>\n                      );\n                    })}\n                  </div>\n                </>\n              ) : null}\n\n              {/* Trending / All */}\n              <p className=\"px-1 pb-1.5 text-[11px] font-semibold uppercase tracking-wider text-(--color-fg-muted)\">\n                {q ? \"Results\" : chainFilter === \"all\" ? \"Trending\" : \"Tokens\"}\n              </p>\n              <ul className=\"flex flex-col gap-0.5\">\n                {filtered.length === 0 ? (\n                  <li className=\"py-8 text-center text-xs text-(--color-fg-muted)\">\n                    No tokens found\n                  </li>\n                ) : null}\n                {filtered.map((t) => {\n                  const chain = chains.find((c) => c.id === t.chainId)!;\n                  const active = t.id === selectedId;\n                  return (\n                    <li key={t.id}>\n                      <button\n                        type=\"button\"\n                        onClick={() => onPick(t.id)}\n                        className={cn(\n                          \"flex w-full items-center justify-between rounded-xl px-2 py-2 text-left transition-colors press\",\n                          active\n                            ? \"bg-(--color-fg)/5\"\n                            : \"hover:bg-(--color-fg)/[0.04]\",\n                        )}\n                      >\n                        <span className=\"flex min-w-0 items-center gap-2.5\">\n                          <TokenDot token={t} chain={chain} size={32} />\n                          <span className=\"flex min-w-0 flex-col\">\n                            <span className=\"truncate text-sm font-semibold text-(--color-fg)\">\n                              {t.name}\n                            </span>\n                            <span className=\"truncate text-[11px] text-(--color-fg-muted)\">\n                              {t.symbol}\n                            </span>\n                          </span>\n                        </span>\n                        <span className=\"shrink-0 text-right text-[11px] tabular-nums text-(--color-fg-muted)\">\n                          {t.address ??\n                            (t.balance ? formatAmount(t.balance) : \"\")}\n                        </span>\n                      </button>\n                    </li>\n                  );\n                })}\n              </ul>\n            </div>\n          </motion.div>\n        </>\n      ) : null}\n    </AnimatePresence>\n  );\n}\n\nfunction ChainChip({\n  active,\n  onClick,\n  chain,\n  label,\n}: {\n  active: boolean;\n  onClick: () => void;\n  chain?: Chain;\n  label?: string;\n}) {\n  return (\n    <button\n      type=\"button\"\n      onClick={onClick}\n      aria-pressed={active}\n      className={cn(\n        \"inline-flex h-9 shrink-0 items-center justify-center rounded-xl border transition-colors press\",\n        chain ? \"w-9\" : \"px-3\",\n        active\n          ? \"border-(--color-fg)/20 bg-(--color-fg)/5 text-(--color-fg)\"\n          : \"border-(--color-border)/60 bg-(--color-bg)/40 text-(--color-fg) hover:border-(--color-border-strong)\",\n      )}\n      title={chain?.name ?? label}\n    >\n      {chain ? (\n        <ChainDot chain={chain} size={20} />\n      ) : (\n        <span className=\"text-xs font-semibold\">{label}</span>\n      )}\n    </button>\n  );\n}\n\n/* ============================================================\n * Bits\n * ============================================================ */\n\nfunction ChainDot({ chain, size = 16 }: { chain: Chain; size?: number }) {\n  return (\n    <span\n      style={{ width: size, height: size, background: chain.color }}\n      className=\"inline-flex shrink-0 items-center justify-center rounded-full text-[9px] font-bold text-white\"\n    >\n      {chain.symbol}\n    </span>\n  );\n}\n\nfunction TokenDot({\n  token,\n  chain,\n  size = 28,\n}: {\n  token: Token;\n  chain: Chain;\n  size?: number;\n}) {\n  return (\n    <span\n      className=\"relative inline-flex shrink-0\"\n      style={{ width: size, height: size }}\n    >\n      <span className=\"absolute inset-0 inline-flex items-center justify-center rounded-full border border-(--color-border) bg-(--color-bg) text-[11px] font-bold text-(--color-fg)\">\n        {token.symbol.slice(0, 2)}\n      </span>\n      <span\n        style={{\n          background: chain.color,\n          width: size * 0.42,\n          height: size * 0.42,\n        }}\n        className=\"absolute -bottom-0.5 -right-0.5 inline-flex items-center justify-center rounded-full border-2 border-(--color-bg-elev) text-[7px] font-bold text-white\"\n      >\n        {chain.symbol}\n      </span>\n    </span>\n  );\n}\n\nfunction isValidAddress(v: string) {\n  if (/^0x[0-9a-fA-F]{40}$/.test(v)) return true;\n  if (/\\.(eth|sol|bnb)$/.test(v) && v.length > 5) return true;\n  return false;\n}\n\nfunction truncateAddress(v: string) {\n  if (v.startsWith(\"0x\") && v.length === 42)\n    return v.slice(0, 6) + \"…\" + v.slice(-4);\n  return v;\n}\n\nfunction sanitizeAmount(v: string) {\n  const cleaned = v.replace(/[^0-9.]/g, \"\");\n  const parts = cleaned.split(\".\");\n  if (parts.length <= 1) return cleaned;\n  return parts[0] + \".\" + parts.slice(1).join(\"\");\n}\n\nfunction formatAmount(n: number, max = 6) {\n  if (!isFinite(n)) return \"0\";\n  if (n === 0) return \"0\";\n  if (n >= 1000)\n    return n.toLocaleString(undefined, { maximumFractionDigits: 2 });\n  return n.toLocaleString(undefined, { maximumFractionDigits: max });\n}\n\n// Drawer ease retained for callers that import the curve.\nexport const SWAP_DRAWER_EASE = DRAWER_EASE;\n"},{"path":"components/previews/motion/swap.preview.tsx","type":"preview","content":"\"use client\";\n\nimport { MultiChainSwap } from \"@/components/motion/swap\";\n\nexport function SwapPreview() {\n  return (\n    <div className=\"flex w-full items-center justify-center\">\n      <MultiChainSwap />\n    </div>\n  );\n}\n"},{"path":"lib/utils.ts","type":"util","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"}]}