# Icon Swap

Swaps icons with a scale-and-blur cross-fade: copy to check, play to pause, sun to moon.

**Category:** Transitions  
**Exports:** `IconSwap`  
**npm dependencies:** motion (+ clsx, tailwind-merge for `cn`)

## Install

```bash
npx shadcn@latest add https://charcoalui.com/r/icon-swap.json
```
The file lands at `components/ui/icon-swap.tsx`; import from `@/components/ui/icon-swap`.

## API — Icon Swap
| Prop | Type | Default | Notes |
| --- | --- | --- | --- |
| `state` | `string` | — | Identifies the current icon; change it to swap. |
| `children` | `ReactNode` | — |  |

## Example — Icon Swap

```tsx
import * as React from "react";
import { Check, Copy, Moon, Pause, Play, Sun } from "lucide-react";
import { IconSwap } from "@/components/ui/icon-swap";

export default function IconSwapDemo() {
  const [copied, setCopied] = React.useState(false);
  const [playing, setPlaying] = React.useState(false);
  const [dark, setDark] = React.useState(true);
  const btn = "grid size-11 place-items-center rounded-xl border bg-card hover:bg-muted [&_svg]:size-5";
  return (
    <div className="flex gap-3">
      <button
        type="button"
        aria-label={copied ? "Copied" : "Copy"}
        className={btn}
        onClick={() => {
          setCopied(true);
          setTimeout(() => setCopied(false), 1400);
        }}
      >
        <IconSwap state={copied ? "check" : "copy"}>{copied ? <Check className="text-success" /> : <Copy />}</IconSwap>
      </button>
      <button type="button" aria-label={playing ? "Pause" : "Play"} className={btn} onClick={() => setPlaying((p) => !p)}>
        <IconSwap state={playing ? "pause" : "play"}>{playing ? <Pause /> : <Play />}</IconSwap>
      </button>
      <button type="button" aria-label="Toggle theme" className={btn} onClick={() => setDark((d) => !d)}>
        <IconSwap state={dark ? "moon" : "sun"}>{dark ? <Moon /> : <Sun />}</IconSwap>
      </button>
    </div>
  );
}
```

## Source — components/ui/icon-swap.tsx

```tsx
"use client";

import * as React from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";

export type IconSwapProps = {
  /** Identifies the current icon. Change it to swap. */
  state: string;
  children: React.ReactNode;
  className?: string;
};

/** Swaps icons with a scale and blur cross-fade: copy to check, play to pause, sun to moon. */
export function IconSwap({ state, children, className }: IconSwapProps) {
  const reduce = useReducedMotion();
  const hidden = reduce ? { opacity: 0 } : { opacity: 0, scale: 0.5, filter: "blur(4px)" };
  return (
    <span className={cn("relative inline-grid place-items-center [&>*]:[grid-area:1/1]", className)}>
      <AnimatePresence mode="popLayout" initial={false}>
        <motion.span
          key={state}
          initial={hidden}
          animate={{ opacity: 1, scale: 1, filter: "blur(0px)" }}
          exit={hidden}
          transition={{ type: "spring", duration: 0.3, bounce: 0.2 }}
          className="inline-grid place-items-center"
        >
          {children}
        </motion.span>
      </AnimatePresence>
    </span>
  );
}
```