# Empty First Project — React Empty State

> A first-run dropzone whose dashed border marches, tightens and tints while a file is dragged over it, with a primary action and starter templates.

- Category: [Empty States](https://pixelpapa.com/components/empty-states.md)
- Tags: empty-state, dropzone, upload, onboarding, drag
- Dependencies: lucide-react, motion (plus React and Tailwind CSS v4)
- Last updated: 2026-11-13
- Live preview: https://pixelpapa.com/components/empty-states/empty-first-project

## Install

```bash
npx shadcn@latest add "https://pixelpapa.com/r/empty-first-project.json"
```

The file is written to `components/pixelpapa/empty-first-project.tsx`. To install manually, run `npm install lucide-react motion` and copy the source below into your project. It imports `cn` from `@/lib/utils`, which every shadcn project has.

## Source

```tsx
"use client";

import { FileUp, FolderPlus, Plus } from "lucide-react";
import { motion } from "motion/react";
import { useRef, useState, type DragEvent } from "react";
import { cn } from "@/lib/utils";

const templates = ["Landing page", "Mobile app", "Design system"];

export default function EmptyFirstProject() {
  const [over, setOver] = useState(false);
  const [dropped, setDropped] = useState<string | null>(null);
  const depth = useRef(0);

  function onDrag(e: DragEvent, delta: number) {
    e.preventDefault();
    depth.current = Math.max(0, depth.current + delta);
    setOver(depth.current > 0);
  }

  function onDrop(e: DragEvent) {
    e.preventDefault();
    depth.current = 0;
    setOver(false);
    setDropped(e.dataTransfer.files[0]?.name ?? "Untitled import");
  }

  return (
    <div className="w-full max-w-md">
      <style>{`@keyframes pp-empty-first-project-march { to { stroke-dashoffset: -28; } }`}</style>
      <motion.div
        onDragEnter={(e) => onDrag(e, 1)}
        onDragLeave={(e) => onDrag(e, -1)}
        onDragOver={(e) => e.preventDefault()}
        onDrop={onDrop}
        animate={{ scale: over ? 1.02 : 1 }}
        transition={{ type: "spring", stiffness: 300, damping: 20 }}
        className={cn("group relative rounded-3xl px-8 py-10 text-center transition-colors duration-300", over ? "bg-primary/5" : "bg-card")}
      >
        <svg className="pointer-events-none absolute inset-0 size-full" aria-hidden>
          <rect
            x="1" y="1" rx="23" ry="23"
            style={{ width: "calc(100% - 2px)", height: "calc(100% - 2px)", animation: "pp-empty-first-project-march 1.2s linear infinite", animationPlayState: over ? "running" : undefined }}
            fill="none" strokeWidth="2" strokeDasharray="8 6" strokeLinecap="round"
            className={cn("transition-colors duration-300 [animation-play-state:paused] group-hover:[animation-play-state:running] motion-reduce:animate-none", over ? "stroke-primary" : "stroke-border group-hover:stroke-muted-foreground/50")}
          />
        </svg>

        <motion.div
          animate={{ y: over ? -6 : 0, rotate: over ? -6 : 0 }}
          transition={{ type: "spring", stiffness: 260, damping: 14 }}
          className="relative mx-auto flex size-16 items-center justify-center"
        >
          <span className="absolute inset-0 rotate-6 rounded-2xl bg-primary/15" />
          <span className="absolute inset-0 -rotate-3 rounded-2xl border border-border bg-background shadow-sm" />
          {over ? <FileUp className="relative size-6 text-primary" /> : <FolderPlus className="relative size-6 text-primary" />}
        </motion.div>

        <h3 className="mt-6 text-xl font-semibold tracking-tight text-card-foreground">
          {over ? "Drop it right here" : dropped ? `Imported ${dropped}` : "Start your first project"}
        </h3>
        <p className="mx-auto mt-2 max-w-xs text-sm text-muted-foreground">
          {dropped ? "We are unpacking the layers. This usually takes a few seconds." : "Drag in a .fig or .sketch file, or begin with a blank canvas. You can invite the team later."}
        </p>

        <button
          type="button"
          onClick={() => setDropped(null)}
          className="relative mt-6 inline-flex h-10 items-center gap-2 rounded-full bg-primary px-5 text-sm font-medium text-primary-foreground shadow-lg shadow-primary/25 transition-transform hover:-translate-y-0.5"
        >
          <Plus className="size-4" /> New project
        </button>

        <div className="relative mt-7 flex flex-wrap items-center justify-center gap-2 text-xs">
          <span className="text-muted-foreground">or start from</span>
          {templates.map((t) => (
            <button key={t} type="button" className="rounded-full border border-border bg-background px-2.5 py-1 font-medium text-foreground transition-colors hover:border-primary hover:text-primary">
              {t}
            </button>
          ))}
        </div>
      </motion.div>
    </div>
  );
}
```

## AI prompt

```text
Add the "Empty First Project" component from PixelPapa UI to this project.

What it is: A first-run dropzone whose dashed border marches, tightens and tints while a file is dragged over it, with a primary action and starter templates.
Category: Empty States. Depends on: lucide-react, motion.

1. Install it with the shadcn CLI:
   npx shadcn@latest add "https://pixelpapa.com/r/empty-first-project.json"
   (If the CLI is unavailable, fetch that URL, write files[0].content to components/pixelpapa/empty-first-project.tsx and install the listed dependencies.)
2. Import it from "@/components/pixelpapa/empty-first-project" and place it where I describe below.
3. Keep its animation timings and accessibility attributes. Replace the demo copy and data with content that fits my product, and use my existing theme tokens (bg-background, text-foreground, bg-primary, border-border) rather than hard-coded colours.

Where to use it: <describe the page and position here>
```

## More empty states

- [Empty Error Retry](https://pixelpapa.com/components/empty-states/empty-error-retry.md) (Pro): A failed-to-load state with a flickering unplugged-cable drawing and a retry button that spins, fails once with a shake, then reconnects with a spark.
- [Empty Inbox Illustration](https://pixelpapa.com/components/empty-states/empty-inbox-illustration.md): An inbox-zero state with an animated SVG tray: a paper plane loops overhead on a dashed trail while the tray bobs and sparkles blink.
- [Empty Search No Results](https://pixelpapa.com/components/empty-states/empty-search-no-results.md): A no-results state with a scanning magnifier, a working search field and suggestion chips that stagger in and fill the query when clicked.
