# Morphing image effect default

A shadcn/ui block from the [@craftui](https://www.craftui.space) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/craftui/morphing-image-effect-default).

- Page, with a live preview: https://shadcn.exchange/r/craftui/morphing-image-effect-default
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/craftui/morphing-image-effect-default-light-1b7810568e8177ed.png
- Registry item: https://www.craftui.space/r/morphing-image-effect-default.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://www.craftui.space/r/morphing-image-effect-default.json
```

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

## Dependencies

- npm packages: `motion`
- Registry items: `https://www.craftui.space/r/craftui-base.json`

## Files

### `components/craftui/blocks/morphing-image-effect/default.tsx`

```tsx
"use client";

import { AnimatePresence, motion } from "motion/react";
import { useState } from "react";

type Stage = 1 | 2 | 3;

const stageConfig: Record<
  Stage,
  {
    imageW: number;
    imageH: number;
    imageRadius: number;
    fontSize: number;
    rotate: number;
  }
> = {
  1: {
    imageW: 54,
    imageH: 80,
    imageRadius: 27,
    fontSize: 88,
    rotate: 0,
  },
  2: {
    imageW: 620,
    imageH: 400,
    imageRadius: 20,
    fontSize: 176,
    rotate: -3,
  },
  3: {
    imageW: 260,
    imageH: 500,
    imageRadius: 28,
    fontSize: 144,
    rotate: 4,
  },
};

const IMAGE_URL =
  "https://images.unsplash.com/photo-1586023492125-27b2c045efd7?w=900&auto=format&fit=crop&q=80";

const stageLabel = (s: Stage) => `Stage 0${s} — click to advance`;

// ease-in-out-quart — for on-screen morphing elements
const morph = {
  ease: [0.77, 0, 0.175, 1] as const,
  duration: 0.65,
};

export default function MorphingImageEffect() {
  const [stage, setStage] = useState<Stage>(1);
  const cfg = stageConfig[stage];

  const advance = () => setStage((s) => ((s % 3) + 1) as Stage);

  return (
    <div
      onClick={advance}
      className="flex min-h-screen cursor-pointer flex-col items-center justify-center overflow-hidden bg-[#eceae4] w-full select-none"
    >
      {/* Grid: 1fr [image auto] 1fr — P and ST always adjacent to image */}
      <div
        className="grid w-full items-center px-8"
        style={{ gridTemplateColumns: "1fr auto 1fr", gap: "0 1rem" }}
      >
        {/* P — right-aligned so it touches the image's left edge */}
        <motion.span
          animate={{ fontSize: cfg.fontSize }}
          transition={{ ...morph, delay: 0.04 }}
          className="text-right font-sans font-normal leading-none tracking-tighter text-[#111]"
        >
          P
        </motion.span>

        {/* Image — CSS t-resize owns width/height, Motion owns borderRadius + rotate */}
        <motion.div
          animate={{ borderRadius: cfg.imageRadius, rotate: cfg.rotate }}
          style={{ width: cfg.imageW, height: cfg.imageH }}
          transition={morph}
          className="t-resize shrink-0 overflow-hidden"
        >
          <img
            src={IMAGE_URL}
            alt="interior"
            className="block h-full w-full object-cover"
          />
        </motion.div>

        {/* ST — left-aligned so it touches the image's right edge */}
        <motion.span
          animate={{ fontSize: cfg.fontSize }}
          transition={{ ...morph, delay: 0.04 }}
          className="text-left font-sans font-normal leading-none tracking-tighter text-[#111]"
        >
          ST
        </motion.span>
      </div>
    </div>
  );
}
```
