# 8-bit Portal Transition

> Interactive CTA with clickable portal and traveling state.

A shadcn/ui block from the [@8bitcn](https://www.8bitcn.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/8bitcn/portal-transition).

- Page, with a live preview: https://shadcn.exchange/r/8bitcn/portal-transition
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/8bitcn/portal-transition-light-acfdc4ed778e8267.png
- Registry item: https://www.8bitcn.com/r/portal-transition.json
- Categories: game
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://www.8bitcn.com/r/portal-transition.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `badge`, `button`

## Files

### `components/ui/8bit/blocks/portal-transition.tsx`

```tsx
"use client";

import Image from "next/image";
import Link from "next/link";
import { useCallback, useState } from "react";

import { cn } from "@/lib/utils";

import { Badge } from "@/components/ui/8bit/badge";
import { Button } from "@/components/ui/8bit/button";

import "@/components/ui/8bit/styles/retro.css";

interface PortalTransitionProps {
  badge?: string;
  className?: string;
  description?: string;
  href?: string;
  hrefSecondary?: string;
  imageSrc?: string;
  onEnter?: () => void;
  primaryLabel?: string;
  secondaryLabel?: string;
  title?: string;
  travelingText?: string;
}

export default function PortalTransition({
  title = "Click to go through the portal",
  description = "The gateway is open. One step and you're in the next realm.",
  badge = "FAST TRAVEL",
  primaryLabel = "Enter Portal",
  secondaryLabel = "Choose Destination",
  travelingText = "Traveling...",
  href = "/docs",
  hrefSecondary = "/docs/blocks",
  imageSrc = "/images/8bit-portal.png",
  onEnter,
  className,
}: PortalTransitionProps) {
  const [traveling, setTraveling] = useState(false);

  const handleEnter = useCallback(() => {
    setTraveling(true);
    if (onEnter) {
      setTimeout(() => {
        onEnter();
        setTraveling(false);
      }, 800);
    } else {
      setTimeout(() => {
        setTraveling(false);
      }, 800);
    }
  }, [onEnter]);

  return (
    <div
      className={cn(
        "retro flex w-full flex-col items-center gap-6 bg-background px-4 py-16 text-center md:py-24",
        className,
      )}
    >
      {badge && (
        <Badge variant="secondary">{traveling ? travelingText : badge}</Badge>
      )}

      <h2 className="retro max-w-md font-bold text-2xl tracking-tight sm:text-3xl">
        {title}
      </h2>

      <p className="retro max-w-sm text-muted-foreground text-[9px]">
        {description}
      </p>

      {/* Portal image */}
      {imageSrc && (
        <button
          aria-label={primaryLabel}
          className={cn(
            "group relative cursor-pointer border-0 bg-transparent p-0 transition-transform duration-300 motion-reduce:transform-none motion-reduce:transition-none",
            traveling ? "scale-110" : "hover:scale-105",
          )}
          disabled={traveling}
          onClick={handleEnter}
          type="button"
        >
          <Image
            alt="Portal"
            className={cn(
              "pixelated transition-opacity duration-300 motion-reduce:transition-none",
              traveling && "animate-pulse motion-reduce:animate-none",
            )}
            height={220}
            src={imageSrc}
            width={220}
          />
        </button>
      )}

      {/* CTAs */}
      <div className="flex flex-wrap justify-center gap-4">
        {href && !traveling ? (
          <Button asChild>
            <Link href={href}>{primaryLabel}</Link>
          </Button>
        ) : (
          <Button disabled={traveling} onClick={href ? undefined : handleEnter}>
            {traveling ? travelingText : primaryLabel}
          </Button>
        )}

        {secondaryLabel && hrefSecondary && (
          <Button asChild variant="outline">
            <Link href={hrefSecondary}>{secondaryLabel}</Link>
          </Button>
        )}
      </div>

      <p className="retro text-muted-foreground text-[8px]">
        {traveling ? "Opening gateway..." : "No cooldown required."}
      </p>
    </div>
  );
}
```

### `components/ui/8bit/styles/retro.css`

```css
@import url("https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap");

.retro {
  font-family:
    "Press Start 2P",
    system-ui,
    -apple-system,
    sans-serif;
  line-height: 1.5;
  letter-spacing: 0.5px;
}

.pixelated {
  image-rendering: pixelated;
  image-rendering: crisp-edges;
}
```
