# Launch Dock: Studio Work

> A design studio's clients as marks in the dock, each launching the site the studio built for them, address bar and all.

A shadcn/ui block from the [@keyframeui](https://keyframeui.com/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/keyframeui/launch-dock-studio-demo).

- Page, with a live preview: https://shadcn.exchange/r/keyframeui/launch-dock-studio-demo
- Screenshot: https://shadcn.exchange/shot/keyframeui/launch-dock-studio-demo/light.png
- Registry item: https://keyframeui.com/r/launch-dock-studio-demo.json
- Categories: showcase, testimonials, motion
- Listed: 2026-11-02, source last changed 2026-11-02

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/keyframeui/launch-dock-studio-demo.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: `https://keyframeui.com/r/launch-dock.json`

## Notes from the registry

Live preview, examples and props: https://keyframeui.com/components/launch-dock#launch-dock-studio-demo

## Files

### `registry/demos/launch-dock-studio-demo.tsx`

```tsx
"use client";

import * as React from "react";

import { LaunchDock, type LaunchDockItem } from "@/components/ui/launch-dock";

const photo = (id: string) => `https://images.unsplash.com/photo-${id}?auto=format&fit=crop&w=1400&h=880&q=80`;

type Project = {
  id: string;
  client: string;
  work: string;
  domain: string;
  mark: React.ReactNode;
};

function Mark({ bg, children }: { bg: string; children: React.ReactNode }) {
  return <span className={`grid size-full place-items-center ${bg}`}>{children}</span>;
}

function Url({ children }: { children: React.ReactNode }) {
  return (
    <span className="inline-flex min-w-[46%] items-center justify-center gap-1.5 rounded-md bg-zinc-950/[0.05] px-3 py-0.5 dark:bg-white/[0.07]">
      <svg viewBox="0 0 12 12" aria-hidden className="size-2.5 opacity-60">
        <path d="M3.5 5.5V4a2.5 2.5 0 0 1 5 0v1.5M2.5 5.5h7v5h-7z" fill="none" stroke="currentColor" strokeWidth="1.2" />
      </svg>
      {children}
    </span>
  );
}

// A client's site as the studio shipped it: one photo, their type, their nav.
function Site({
  src,
  alt,
  tone,
  nav,
  brand,
  children,
  shade = "bg-gradient-to-b from-black/55 via-black/10 to-transparent",
}: {
  src: string;
  alt: string;
  tone: "light" | "dark";
  nav: string[];
  brand: React.ReactNode;
  children: React.ReactNode;
  shade?: string;
}) {
  return (
    <div className={`relative h-full overflow-hidden ${tone === "light" ? "text-white" : "text-zinc-950"}`}>
      {/* eslint-disable-next-line @next/next/no-img-element */}
      <img src={src} alt={alt} className="absolute inset-0 size-full object-cover" />
      <div className={`absolute inset-0 ${shade}`} />
      <header className="relative flex items-center justify-between px-10 py-7">
        {brand}
        <nav className="flex gap-7 text-[13px] font-medium opacity-90">
          {nav.map((link) => (
            <span key={link}>{link}</span>
          ))}
        </nav>
      </header>
      <div className="relative px-10 pt-14">{children}</div>
    </div>
  );
}

const projects: Project[] = [
  {
    id: "halden",
    client: "Halden House",
    work: "Website and identity, 2026",
    domain: "halden.house",
    mark: (
      <Mark bg="bg-stone-200">
        <span className="font-serif text-[1.65em] text-stone-800 italic">H</span>
      </Mark>
    ),
  },
  {
    id: "ember",
    client: "Ember",
    work: "Launch campaign, 2026",
    domain: "ember.run",
    mark: (
      <Mark bg="bg-red-600">
        <span className="text-[1.5em] font-black text-white italic">E</span>
      </Mark>
    ),
  },
  {
    id: "tempo",
    client: "Tempo",
    work: "Product launch site, 2025",
    domain: "tempo.watch",
    mark: (
      <Mark bg="bg-slate-900">
        <span className="text-[1.35em] font-light tracking-widest text-white">T</span>
      </Mark>
    ),
  },
  {
    id: "sable",
    client: "Sable",
    work: "Shop and art direction, 2025",
    domain: "sable.store",
    mark: (
      <Mark bg="bg-zinc-950">
        <span className="font-serif text-[1.55em] text-amber-100">S</span>
      </Mark>
    ),
  },
  {
    id: "nordlys",
    client: "Nordlys",
    work: "Booking site, 2024",
    domain: "nordlys.no",
    mark: (
      <Mark bg="bg-teal-700">
        <span className="text-[1.4em] font-semibold text-teal-50">N</span>
      </Mark>
    ),
  },
  {
    id: "carry",
    client: "Carry Co.",
    work: "Packaging and shop, 2024",
    domain: "carry.co",
    mark: (
      <Mark bg="bg-amber-200">
        <span className="font-mono text-[1.35em] font-bold text-amber-900">C</span>
      </Mark>
    ),
  },
];

const sites: Record<string, React.ReactNode> = {
  halden: (
    <Site
      src={photo("1600585154340-be6161a56a0c")}
      alt="A timber house at dusk with its windows lit"
      tone="light"
      nav={["Projects", "Practice", "Journal", "Contact"]}
      brand={<span className="font-serif text-[22px] italic">Halden House</span>}
      shade="bg-gradient-to-br from-black/75 via-black/30 to-black/0"
    >
      <h1 className="max-w-[560px] font-serif text-[54px] leading-[1.04] tracking-[-0.01em]">
        Houses that hold the evening light.
      </h1>
      <p className="mt-5 max-w-[400px] text-[15px] leading-relaxed text-white/80">
        A small practice on the west coast, building eight homes a year in timber, stone and glass.
      </p>
    </Site>
  ),
  ember: (
    <Site
      src={photo("1542291026-7eec264c27ff")}
      alt="A red running shoe on a red background"
      tone="light"
      nav={["Run", "Train", "Stories", "Bag (0)"]}
      brand={<span className="text-[22px] font-black tracking-tight italic">EMBER</span>}
      shade="bg-gradient-to-b from-black/45 via-transparent to-transparent"
    >
      <h1 className="text-[92px] leading-[0.86] font-black tracking-[-0.04em] uppercase italic">
        Run
        <br />
        warm.
      </h1>
      <span className="mt-7 inline-block rounded-full bg-white px-6 py-3 text-[14px] font-bold text-red-600">
        Shop the Ember Runner · $140
      </span>
    </Site>
  ),
  tempo: (
    <Site
      src={photo("1523275335684-37898b6baf30")}
      alt="A white watch on a pale background"
      tone="dark"
      nav={["Collection", "Craft", "Stores"]}
      brand={<span className="text-[18px] font-light tracking-[0.5em]">TEMPO</span>}
      shade="bg-gradient-to-b from-white/70 via-white/20 to-transparent"
    >
      <h1 className="max-w-[420px] text-[46px] leading-[1.1] font-light tracking-[-0.02em]">Time, kept quietly.</h1>
      <p className="mt-4 max-w-[340px] text-[15px] leading-relaxed text-zinc-700">
        The Model 01 has no logo on its face and no second hand. Just the hour, and enough of the minute.
      </p>
    </Site>
  ),
  sable: (
    <Site
      src={photo("1592078615290-033ee584e267")}
      alt="A black shell chair against a light wall"
      tone="dark"
      nav={["Chairs", "Tables", "Lighting", "Cart"]}
      brand={<span className="font-serif text-[24px]">Sable</span>}
      shade="bg-gradient-to-b from-white/80 via-white/10 to-transparent"
    >
      <p className="text-[12px] font-semibold tracking-[0.2em] uppercase text-zinc-600">New · The Shell Chair</p>
      <h1 className="mt-3 max-w-[460px] font-serif text-[58px] leading-[1.02] tracking-[-0.02em]">Sit with it a while.</h1>
    </Site>
  ),
  nordlys: (
    <Site
      src={photo("1600607687939-ce8a6c25118c")}
      alt="A bright open living room with large windows"
      tone="light"
      nav={["Homes", "Regions", "About"]}
      brand={<span className="text-[20px] font-semibold tracking-tight">nordlys</span>}
      shade="bg-gradient-to-b from-black/50 via-black/15 to-transparent"
    >
      <h1 className="max-w-[520px] text-[48px] leading-[1.06] font-semibold tracking-[-0.03em]">
        Stay somewhere that feels like yours.
      </h1>
      <div className="mt-7 flex w-[560px] items-center gap-1 rounded-2xl bg-white p-1.5 text-[13px] text-zinc-500 shadow-xl">
        <span className="flex-1 rounded-xl px-4 py-2.5">Lofoten, Norway</span>
        <span className="h-6 w-px bg-zinc-200" />
        <span className="flex-1 rounded-xl px-4 py-2.5">Feb 14 – 18</span>
        <span className="h-6 w-px bg-zinc-200" />
        <span className="flex-1 rounded-xl px-4 py-2.5">2 guests</span>
        <span className="rounded-xl bg-teal-700 px-5 py-2.5 font-semibold text-white">Search</span>
      </div>
    </Site>
  ),
  carry: (
    <Site
      src={photo("1544816155-12df9643f363")}
      alt="A canvas tote bag hanging on a wall"
      tone="dark"
      nav={["Totes", "Duffels", "Repairs", "Bag"]}
      brand={<span className="font-mono text-[18px] font-bold">CARRY CO.</span>}
      shade="bg-gradient-to-b from-amber-50/85 via-amber-50/20 to-transparent"
    >
      <h1 className="max-w-[460px] font-mono text-[44px] leading-[1.08] font-bold tracking-[-0.03em]">
        Made to be overfilled.
      </h1>
      <p className="mt-4 max-w-[360px] text-[15px] leading-relaxed text-zinc-700">
        18oz canvas, double-stitched handles, and free repairs for as long as you own one.
      </p>
    </Site>
  ),
};

const items: (LaunchDockItem & { work: string })[] = projects.map((project) => ({
  id: project.id,
  label: project.client,
  title: <Url>{project.domain}</Url>,
  icon: project.mark,
  content: sites[project.id],
  work: project.work,
}));

export function LaunchDockStudioDemo() {
  const [open, setOpen] = React.useState(items[0].id);
  const project = items.find((item) => item.id === open) ?? items[0];

  return (
    <div className="flex w-full max-w-3xl flex-col items-center gap-5">
      <LaunchDock items={items} value={open} onValueChange={setOpen} autoplay dockLabel="Clients" />
      <p aria-live="polite" className="text-sm text-zinc-500 dark:text-zinc-400">
        <span className="font-medium text-zinc-900 dark:text-zinc-100">{project.label}</span> · {project.work}
      </p>
    </div>
  );
}
```
