Below is a tight drop-in that covers both cases:

SVG → we scrub any <rect fill="white">, <background> blocks, and force the root <svg> to fill="none" where needed.

PNG/JPG (if one sneaks in) → we call a tiny /api/image/remove-bg route using your existing Replicate token (rembg).

1) Add the wand button to your Preview

Where you render the preview box, wrap it in a relative container and overlay an icon button in the top-right.

// inside your preview card
<div className="relative rounded-2xl border p-4">
  <div className="mb-2 text-sm opacity-70">Preview</div>

  {/* Wand button */}
  <button
    type="button"
    onClick={handleMagicWand}
    disabled={!assetUrl} // disable if nothing loaded
    title="Magic Wand: remove background"
    className="absolute right-3 top-3 z-10 rounded-md border bg-white/90 px-2 py-1 text-xs shadow hover:bg-white disabled:opacity-40"
  >
    {/* simple wand icon */}
    <span aria-hidden>✨</span>
  </button>

  <div
    className="flex items-center justify-center rounded-xl bg-white"
    style={{
      height: 280,
      border: "1px dashed #d0d7de",
      background:
        "linear-gradient(45deg, #eee 25%, transparent 25%)," +
        "linear-gradient(-45deg, #eee 25%, transparent 25%)," +
        "linear-gradient(45deg, transparent 75%, #eee 75%)," +
        "linear-gradient(-45deg, transparent 75%, #eee 75%)",
      backgroundSize: "20px 20px",
      backgroundPosition: "0 0, 0 10px, 10px -10px, -10px 0px",
    }}
  >
    {!assetUrl && <div className="text-sm opacity-60">No logo generated yet</div>}
    {assetUrl && (
      <img
        src={assetUrl}
        alt="Preview"
        style={{ maxWidth: "100%", maxHeight: "100%", objectFit: "contain", display: "block" }}
      />
    )}
  </div>
</div>


assetUrl should be the current image you’re previewing (SVG or PNG/JPG).
We’ll update it to the cleaned/transparent version after the wand runs.

2) The magic wand handler

Add this alongside your component logic. It detects SVG vs raster, then cleans appropriately.

const [assetUrl, setAssetUrl] = useState<string | null>(null); // your current preview URL

async function handleMagicWand() {
  if (!assetUrl) return;

  // Case A: SVG → clean locally
  if (isSvgUrl(assetUrl)) {
    const res = await fetch(assetUrl, { cache: "no-store" });
    let svg = await res.text();
    const cleaned = cleanSvg(svg);
    const blob = new Blob([cleaned], { type: "image/svg+xml;charset=utf-8" });
    const url = URL.createObjectURL(blob);
    setAssetUrl(url);
    return;
  }

  // Case B: Raster (PNG/JPG) → call remove-bg API
  const resp = await fetch("/api/image/remove-bg", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ imageUrl: assetUrl }),
  });
  const json = await resp.json();
  if (!resp.ok) {
    alert(json.error || "Background removal failed");
    return;
  }
  setAssetUrl(json.cleanedUrl); // transparent PNG URL
}

/* helpers */
function isSvgUrl(u: string) {
  const lower = u.toLowerCase();
  return lower.endsWith(".svg") || lower.includes(".svg?");
}

// robust SVG scrubber
function cleanSvg(svg: string) {
  // remove obvious background rects (white/near-white)
  let out = svg
    .replace(/<rect[^>]*fill="(?:#fff(?:fff)?|white|rgb\(255,\s*255,\s*255\))"[^>]*\/?>/gi, "")
    .replace(/<background[^>]*>[\s\S]*?<\/background>/gi, "");

  // remove any full-canvas rects regardless of fill if they sit at root-level background
  out = out.replace(/<rect[^>]*x="0"[^>]*y="0"[^>]*width="100%?"[^>]*height="100%?"[^>]*\/?>/gi, "");

  // ensure root svg has no background fill
  out = out.replace(/<svg\b([^>]*?)style="[^"]*background[^"]*"([^>]*)>/i, "<svg$1$2>");

  // force transparent background via style if needed
  if (!/background: ?none/i.test(out)) {
    out = out.replace(
      /<svg\b([^>]*)>/i,
      (_m, attrs) => `<svg${attrs} style="background:none">`
    );
  }
  return out;
}

3) Server route for raster background removal (Replicate rembg)

Create a small API route that takes an image URL (PNG/JPG), calls rembg, and returns a transparent PNG URL.

src/app/api/image/remove-bg/route.ts

export const runtime = "nodejs";

import { NextRequest, NextResponse } from "next/server";

const REPLICATE_TOKEN = process.env.REPLICATE_API_TOKEN!;
// Public rembg model on Replicate
const REMBG_MODEL = "cjwbw/rembg"; // or "xinntao/rembg" variants; you can swap if preferred
const REMBG_VERSION = ""; // optional; can omit to use model default

export async function POST(req: NextRequest) {
  try {
    if (!REPLICATE_TOKEN) {
      return NextResponse.json({ error: "Missing REPLICATE_API_TOKEN" }, { status: 500 });
    }
    const { imageUrl } = await req.json();
    if (!imageUrl) return NextResponse.json({ error: "Missing imageUrl" }, { status: 400 });

    // Kick off prediction
    const create = await fetch("https://api.replicate.com/v1/predictions", {
      method: "POST",
      headers: {
        Authorization: `Token ${REPLICATE_TOKEN}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        // either use the full model+version or a hosted deployment if you made one
        version: REMBG_VERSION || undefined,
        // replicate also accepts "model": "cjwbw/rembg" if using the combined endpoint:
        // but we'll just stick with predictions + (optional) version.
        input: {
          image: imageUrl,
          // Some rembg builds allow params like:
          // "post_process_mask": true,
          // "only_mask": false,
        },
        // Hint Replicate to the correct model via the "model" key if not specifying version:
        model: REMBG_MODEL,
      }),
    });

    const createTxt = await create.text();
    if (!create.ok) {
      return NextResponse.json({ error: `Create failed: ${createTxt}` }, { status: create.status || 502 });
    }
    const job = JSON.parse(createTxt);
    const id = job.id;
    if (!id) return NextResponse.json({ error: "No prediction id" }, { status: 502 });

    // Poll
    const end = Date.now() + 120_000;
    let outUrl: string | null = null;

    while (Date.now() < end) {
      await new Promise((r) => setTimeout(r, 2000));
      const st = await fetch(`https://api.replicate.com/v1/predictions/${id}`, {
        headers: { Authorization: `Token ${REPLICATE_TOKEN}` },
        cache: "no-store",
      });
      const raw = await st.text();
      if (!st.ok) return NextResponse.json({ error: `Status failed: ${raw}` }, { status: st.status || 502 });

      const data = JSON.parse(raw);
      if (data.status === "succeeded") {
        // rembg typically returns a single PNG URL
        if (Array.isArray(data.output)) {
          outUrl = data.output.find((u: any) => typeof u === "string");
        } else if (typeof data.output === "string") {
          outUrl = data.output;
        } else if (data.output?.image) {
          outUrl = data.output.image;
        }
        break;
      }
      if (data.status === "failed") {
        return NextResponse.json({ error: "Background removal failed" }, { status: 502 });
      }
    }

    if (!outUrl) return NextResponse.json({ error: "Timed out getting cleaned image" }, { status: 504 });

    return NextResponse.json({ cleanedUrl: outUrl });
  } catch (e: any) {
    return NextResponse.json({ error: e?.message ?? "Server error" }, { status: 500 });
  }
}


This gives you a magic-eraser for non-SVG assets.
If you want to store the cleaned image to S3/R2 and return your own CDN URL, you can fetch outUrl, upload, then return that instead.

Notes / Tips

SVGs should already be transparent unless the model draws a rectangle. Our cleanSvg() guarantees transparency by removing those elements.

PNGs can be cleaned server-side with rembg; expect small latency (a few seconds).

You can show a tiny spinner on the Wand button while it runs.