Six apps built on one sync platform, each with its own window. Autoplay walks through them until you pick one, and the line under the dock follows the open app.
registry/demos/launch-dock-demo.tsx
"use client";
import * as React from "react";
import { LaunchDock, type LaunchDockItem } from "@/components/ui/launch-dock";
const photo = (id: string, w: number, h: number) =>
`https://images.unsplash.com/photo-${id}?auto=format&fit=crop&w=${w}&h=${h}&q=80`;
/* ── Icons ─────────────────────────────────────────────── */
function Tile({ from, to, children }: { from: string; to: string; children: React.ReactNode }) {
const id = React.useId();
return (
<svg viewBox="0 0 64 64" aria-hidden>
<defs>
<linearGradient id={`${id}-bg`} x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stopColor={from} />
<stop offset="1" stopColor={to} />
</linearGradient>
<linearGradient id={`${id}-sheen`} x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stopColor="#fff" stopOpacity="0.28" />
<stop offset="0.5" stopColor="#fff" stopOpacity="0" />
</linearGradient>
</defs>
<rect width="64" height="64" fill={`url(#${id}-bg)`} />
<rect width="64" height="64" fill={`url(#${id}-sheen)`} />
{children}
</svg>
);
}
const icons = {
plume: (
<Tile from="#7c83ff" to="#4338ca">
<path d="M44 15c-11 1-21 9-24 21l-3 12 3 1 4-9c9-1 17-7 20-16-4 2-7 3-11 3 5-3 9-7 11-12Z" fill="#fff" />
</Tile>
),
cadence: (
<Tile from="#ff8a73" to="#e8433f">
<rect x="16" y="18" width="32" height="30" rx="6" fill="#fff" />
<rect x="16" y="18" width="32" height="8" rx="4" fill="#ffd6cf" />
<g fill="#e8433f">
<rect x="21" y="31" width="5" height="5" rx="1.5" />
<rect x="29.5" y="31" width="5" height="5" rx="1.5" opacity="0.35" />
<rect x="38" y="31" width="5" height="5" rx="1.5" opacity="0.35" />
<rect x="21" y="39" width="5" height="5" rx="1.5" opacity="0.35" />
<rect x="29.5" y="39" width="5" height="5" rx="1.5" opacity="0.35" />
</g>
</Tile>
),
tally: (
<Tile from="#34d399" to="#047857">
<g fill="#fff">
<rect x="17" y="34" width="7" height="13" rx="2" opacity="0.7" />
<rect x="28.5" y="26" width="7" height="21" rx="2" opacity="0.85" />
<rect x="40" y="17" width="7" height="30" rx="2" />
</g>
</Tile>
),
loop: (
<Tile from="#fb7185" to="#f97316">
<circle cx="32" cy="32" r="15" fill="none" stroke="#fff" strokeWidth="5" />
<circle cx="32" cy="32" r="4.5" fill="#fff" />
</Tile>
),
tessera: (
<Tile from="#fde047" to="#f59e0b">
<rect x="15" y="15" width="20" height="20" rx="5" fill="#fff" />
<circle cx="39" cy="39" r="11" fill="#fff" fillOpacity="0.7" />
</Tile>
),
pigeon: (
<Tile from="#38bdf8" to="#0369a1">
<path d="M17 22a7 7 0 0 1 7-7h16a7 7 0 0 1 7 7v12a7 7 0 0 1-7 7h-9l-8 7v-7a6 6 0 0 1-6-6Z" fill="#fff" />
<g fill="#0ea5e9">
<circle cx="25" cy="28" r="2.5" />
<circle cx="32" cy="28" r="2.5" />
<circle cx="39" cy="28" r="2.5" />
</g>
</Tile>
),
};
/* ── Shared bits for the app windows ───────────────────── */
const ink = "text-zinc-900 dark:text-zinc-100";
const muted = "text-zinc-500 dark:text-zinc-400";
const hairline = "border-zinc-950/[0.07] dark:border-white/[0.07]";
function Face({ initials, className }: { initials: string; className: string }) {
return (
<span
className={`grid size-7 shrink-0 place-items-center rounded-full text-[11px] font-semibold text-white ring-2 ring-white dark:ring-zinc-900 ${className}`}
>
{initials}
</span>
);
}
function Side({ title, children }: { title: string; children: React.ReactNode }) {
return (
<aside className={`flex w-[212px] shrink-0 flex-col gap-6 border-r bg-zinc-50/70 px-4 py-5 dark:bg-white/[0.02] ${hairline}`}>
<div className={`flex items-center gap-2 text-[14px] font-semibold ${ink}`}>{title}</div>
{children}
</aside>
);
}
function SideRow({ children, on, count }: { children: React.ReactNode; on?: boolean; count?: number }) {
return (
<div
className={`flex items-center justify-between rounded-md px-2 py-1.5 text-[13px] ${
on ? `bg-zinc-950/[0.05] font-medium dark:bg-white/[0.07] ${ink}` : muted
}`}
>
<span className="truncate">{children}</span>
{count ? <span className="text-[11px] tabular-nums opacity-70">{count}</span> : null}
</div>
);
}
/* ── The apps ──────────────────────────────────────────── */
function PlumeApp() {
return (
<div className="flex h-full bg-white dark:bg-zinc-900">
<Side title="Halden Lab">
<div className="flex flex-col gap-0.5">
<SideRow count={4}>Inbox</SideRow>
<SideRow>Drafts</SideRow>
<SideRow>Shared with me</SideRow>
</div>
<div className="flex flex-col gap-0.5">
<p className={`px-2 pb-1 text-[11px] font-medium uppercase tracking-wide ${muted}`}>Research</p>
<SideRow on>Field notes: Lisbon</SideRow>
<SideRow>Interview guide v3</SideRow>
<SideRow>Survey synthesis</SideRow>
<SideRow>Q4 research plan</SideRow>
</div>
</Side>
<main className="relative flex-1 px-14 py-9">
<div className="flex items-center justify-between">
<p className={`text-[12px] ${muted}`}>Research / Field notes</p>
<div className="flex items-center gap-2">
<div className="flex -space-x-2">
<Face initials="IM" className="bg-indigo-500" />
<Face initials="TS" className="bg-amber-500" />
<Face initials="MC" className="bg-emerald-500" />
</div>
<span className={`text-[12px] ${muted}`}>3 editing</span>
</div>
</div>
<h1 className={`mt-8 text-[30px] font-semibold tracking-[-0.02em] ${ink}`}>Field notes: Lisbon offsite</h1>
<p className={`mt-2 text-[12px] ${muted}`}>Edited just now by Inês Moreau</p>
<div className={`mt-7 max-w-[456px] space-y-4 text-[14.5px] leading-[1.7] ${ink}`}>
<p>
Six interviews<span className="relative mx-px inline-block h-[18px] w-0.5 translate-y-[3px] bg-amber-500">
<span className="absolute -top-5 left-0 rounded-sm bg-amber-500 px-1 py-px text-[10px] font-semibold whitespace-nowrap text-white">
Tomás
</span>
</span>{" "}
over two days, all with people who run small studios. The pattern was the same
every time: the work happens in a shared doc, and the doc is never where the decision ends up.
</p>
<p>
<span className="rounded-sm bg-indigo-500/15 px-0.5">
Nobody trusts the copy they're looking at to be the latest one
</span>
, so they paste it into chat to be sure. Two of them keep a printed version
on the wall.
</p>
</div>
<div className={`absolute top-[236px] right-7 w-[176px] rounded-lg border bg-white p-3 shadow-sm dark:bg-zinc-800 ${hairline}`}>
<div className="flex items-center gap-2">
<Face initials="IM" className="size-5 bg-indigo-500 text-[9px] ring-0" />
<span className={`text-[12px] font-medium ${ink}`}>Inês</span>
</div>
<p className={`mt-1.5 text-[12px] leading-snug ${muted}`}>Can we lead the readout with this line?</p>
</div>
</main>
</div>
);
}
const week = ["Mon 9", "Tue 10", "Wed 11", "Thu 12", "Fri 13"];
const events = [
{ day: 0, start: 10, end: 11, title: "Design crit", tone: "bg-rose-500/15 text-rose-700 dark:text-rose-300 border-rose-500" },
{ day: 1, start: 9.5, end: 10, title: "1:1 Priya", tone: "bg-sky-500/15 text-sky-700 dark:text-sky-300 border-sky-500" },
{ day: 1, start: 13, end: 15, title: "Pricing workshop", tone: "bg-amber-500/15 text-amber-700 dark:text-amber-300 border-amber-500" },
{ day: 2, start: 11, end: 12.5, title: "Launch review", tone: "bg-rose-500/15 text-rose-700 dark:text-rose-300 border-rose-500" },
{ day: 3, start: 9, end: 12, title: "Focus: Q4 plan", tone: "bg-zinc-500/10 text-zinc-600 dark:text-zinc-300 border-zinc-400" },
{ day: 3, start: 14, end: 15, title: "Hiring sync", tone: "bg-emerald-500/15 text-emerald-700 dark:text-emerald-300 border-emerald-500" },
{ day: 4, start: 15, end: 16.5, title: "Offsite planning", tone: "bg-sky-500/15 text-sky-700 dark:text-sky-300 border-sky-500" },
];
function CadenceApp() {
const hour = 52;
return (
<div className="flex h-full flex-col bg-white dark:bg-zinc-900">
<header className={`flex items-center justify-between border-b px-6 py-4 ${hairline}`}>
<div className="flex items-baseline gap-3">
<h1 className={`text-[20px] font-semibold tracking-[-0.01em] ${ink}`}>November 2026</h1>
<span className={`text-[13px] ${muted}`}>Week 46</span>
</div>
<div className="flex items-center gap-2">
<span className={`rounded-md border px-2.5 py-1 text-[12px] font-medium ${hairline} ${ink}`}>Today</span>
<div className="flex rounded-md bg-zinc-950/[0.05] p-0.5 text-[12px] dark:bg-white/[0.06]">
<span className={`rounded px-2.5 py-0.5 ${muted}`}>Day</span>
<span className={`rounded bg-white px-2.5 py-0.5 font-medium shadow-sm dark:bg-zinc-700 ${ink}`}>Week</span>
<span className={`rounded px-2.5 py-0.5 ${muted}`}>Month</span>
</div>
</div>
</header>
<div className="flex flex-1 overflow-hidden">
<div className="w-14 shrink-0 pt-10">
{[9, 10, 11, 12, 13, 14, 15, 16].map((h) => (
<div key={h} style={{ height: hour }} className={`-mt-2 pr-2 text-right text-[11px] tabular-nums ${muted}`}>
{h > 12 ? h - 12 : h} {h >= 12 ? "PM" : "AM"}
</div>
))}
</div>
{week.map((day, d) => (
<div key={day} className={`relative flex-1 border-l ${hairline}`}>
<div className={`flex h-10 items-center gap-1.5 px-3 text-[12px] ${d === 3 ? "font-semibold text-rose-600 dark:text-rose-400" : muted}`}>
{day}
</div>
{[0, 1, 2, 3, 4, 5, 6, 7].map((h) => (
<div key={h} style={{ height: hour }} className={`border-t ${hairline}`} />
))}
{events
.filter((event) => event.day === d)
.map((event) => (
<div
key={event.title}
style={{ top: 40 + (event.start - 9) * hour + 2, height: (event.end - event.start) * hour - 4 }}
className={`absolute inset-x-1.5 overflow-hidden rounded-md border-l-[3px] px-2 py-1 text-[11.5px] leading-tight font-medium ${event.tone}`}
>
{event.title}
</div>
))}
{d === 3 ? (
<div style={{ top: 40 + 1.6 * hour }} className="absolute -left-1 right-0 flex items-center">
<span className="size-2 rounded-full bg-rose-500" />
<span className="h-px flex-1 bg-rose-500" />
</div>
) : null}
</div>
))}
</div>
</div>
);
}
const spend = [32, 18, 44, 26, 12, 58, 22, 30, 16, 40, 24, 64, 20, 28, 36, 14, 48, 26, 18, 34, 52, 20, 30, 12];
const ledger = [
{ name: "Salary · Halden Lab", when: "Nov 1", amount: "+$4,200.00", plus: true },
{ name: "Rent · Calle da Luna", when: "Nov 1", amount: "−$1,350.00" },
{ name: "Bakehouse on Rua Nova", when: "Today", amount: "−$8.40" },
{ name: "Metro pass", when: "Yesterday", amount: "−$42.00" },
{ name: "Livraria Sá da Costa", when: "Nov 10", amount: "−$27.90" },
];
function TallyApp() {
return (
<div className="flex h-full bg-zinc-50 dark:bg-zinc-950">
<nav className={`flex w-16 shrink-0 flex-col items-center gap-4 border-r bg-white py-5 dark:bg-zinc-900 ${hairline}`}>
<span className="size-8 rounded-lg bg-emerald-600" />
{[0, 1, 2, 3].map((i) => (
<span key={i} className={`size-7 rounded-md ${i === 0 ? "bg-emerald-500/15" : "bg-zinc-950/[0.05] dark:bg-white/[0.06]"}`} />
))}
</nav>
<main className="flex flex-1 flex-col gap-5 p-7">
<div className="flex items-end justify-between">
<div>
<p className={`text-[13px] ${muted}`}>Left to spend in November</p>
<p className={`mt-1 text-[38px] font-semibold tracking-[-0.03em] tabular-nums ${ink}`}>$2,481.20</p>
</div>
<span className="rounded-full bg-emerald-500/15 px-3 py-1 text-[12px] font-medium text-emerald-700 dark:text-emerald-300">
$312 under plan
</span>
</div>
<div className={`rounded-xl border bg-white p-5 dark:bg-zinc-900 ${hairline}`}>
<div className="flex items-center justify-between">
<p className={`text-[13px] font-medium ${ink}`}>Daily spending</p>
<p className={`text-[12px] ${muted}`}>Nov 1 – 24</p>
</div>
<div className="mt-4 flex h-[92px] items-end gap-[7px]">
{spend.map((value, i) => (
<span
key={i}
style={{ height: `${value * 1.4}px` }}
className={`flex-1 rounded-t-[3px] ${i === spend.length - 1 ? "bg-emerald-500" : "bg-emerald-500/30"}`}
/>
))}
</div>
</div>
<div className={`flex-1 rounded-xl border bg-white px-5 py-2 dark:bg-zinc-900 ${hairline}`}>
{ledger.map((row) => (
<div key={row.name} className={`flex items-center justify-between border-b py-3 last:border-0 ${hairline}`}>
<div>
<p className={`text-[13.5px] font-medium ${ink}`}>{row.name}</p>
<p className={`text-[12px] ${muted}`}>{row.when}</p>
</div>
<p className={`text-[13.5px] font-medium tabular-nums ${row.plus ? "text-emerald-600 dark:text-emerald-400" : ink}`}>
{row.amount}
</p>
</div>
))}
</div>
</main>
</div>
);
}
const tracks = [
{ name: "Low Sun", time: "3:41" },
{ name: "Yellow Hours", time: "4:12" },
{ name: "Seawall", time: "2:58" },
{ name: "Tram 28", time: "3:33" },
{ name: "Almost Home", time: "5:06" },
];
function LoopApp() {
return (
<div className="flex h-full flex-col bg-zinc-950 text-white">
<div className="flex flex-1 overflow-hidden">
<Side title="Loop">
<div className="flex flex-col gap-0.5 [&>div]:text-zinc-400">
<SideRow>Listen now</SideRow>
<SideRow>Radio</SideRow>
<SideRow on>Library</SideRow>
</div>
</Side>
<main className="flex-1 overflow-hidden p-8">
<div className="flex items-end gap-6">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={photo("1505740420928-5e560c06d30e", 360, 360)}
alt="Album cover: yellow headphones on a yellow wall"
className="size-[172px] rounded-xl object-cover shadow-2xl shadow-black/50"
/>
<div>
<p className="text-[12px] font-medium tracking-wide text-orange-300 uppercase">Album · 2026</p>
<h1 className="mt-2 text-[34px] font-semibold tracking-[-0.03em]">Yellow Hours</h1>
<p className="mt-1 text-[14px] text-zinc-400">Saint Juniper · 11 songs, 41 min</p>
<div className="mt-4 flex gap-2">
<span className="rounded-full bg-gradient-to-r from-rose-500 to-orange-500 px-5 py-2 text-[13px] font-semibold">
Play
</span>
<span className="rounded-full bg-white/10 px-5 py-2 text-[13px] font-medium">Shuffle</span>
</div>
</div>
</div>
<div className="mt-7">
{tracks.map((track, i) => (
<div
key={track.name}
className={`flex items-center gap-4 rounded-lg px-3 py-2.5 text-[13.5px] ${i === 1 ? "bg-white/[0.07]" : ""}`}
>
<span className={`w-4 text-right tabular-nums ${i === 1 ? "text-orange-400" : "text-zinc-500"}`}>{i + 1}</span>
<span className={`flex-1 ${i === 1 ? "font-medium text-orange-300" : "text-zinc-200"}`}>{track.name}</span>
<span className="text-zinc-500 tabular-nums">{track.time}</span>
</div>
))}
</div>
</main>
</div>
<footer className="flex items-center gap-4 border-t border-white/[0.07] bg-zinc-900 px-6 py-3.5">
<span className="text-[13px] font-medium">Yellow Hours</span>
<span className="text-[12px] text-zinc-500 tabular-nums">1:48</span>
<span className="h-1 flex-1 rounded-full bg-white/10">
<span className="block h-full w-[43%] rounded-full bg-gradient-to-r from-rose-500 to-orange-400" />
</span>
<span className="text-[12px] text-zinc-500 tabular-nums">4:12</span>
</footer>
</div>
);
}
const notes = [
{ x: 96, y: 118, r: -3, tone: "bg-yellow-200 text-yellow-950", text: "Ask for the team size before the plan" },
{ x: 290, y: 96, r: 2, tone: "bg-pink-200 text-pink-950", text: "Skip the tour if they've imported" },
{ x: 484, y: 132, r: -1.5, tone: "bg-sky-200 text-sky-950", text: "First doc opens with their own data" },
{ x: 678, y: 104, r: 3, tone: "bg-lime-200 text-lime-950", text: "Invite step: suggest from email domain" },
];
function TesseraApp() {
return (
<div className="relative h-full overflow-hidden bg-zinc-50 [background-image:radial-gradient(rgb(0_0_0/0.12)_1px,transparent_1px)] [background-size:22px_22px] dark:bg-zinc-900 dark:[background-image:radial-gradient(rgb(255_255_255/0.1)_1px,transparent_1px)]">
<div className={`absolute top-[56px] left-[64px] h-[300px] w-[832px] rounded-2xl border-2 border-dashed border-amber-500/50`}>
<span className="absolute -top-7 left-0 text-[13px] font-semibold text-amber-600 dark:text-amber-400">
Onboarding flow v2
</span>
</div>
{notes.map((note, i) => (
<div
key={note.text}
style={{ left: note.x, top: note.y, rotate: `${note.r}deg` }}
className={`absolute flex size-[150px] flex-col justify-between rounded-sm p-3.5 text-[15px] leading-snug font-medium shadow-md ${note.tone}`}
>
{note.text}
<span className="text-[11px] font-normal opacity-60">Step {i + 1}</span>
</div>
))}
<svg className="absolute inset-0 size-full" aria-hidden>
<g fill="none" stroke="currentColor" strokeWidth="2" strokeDasharray="5 6" className="text-amber-500/70">
<path d="M246 196 C 262 196, 274 186, 290 180" />
<path d="M440 176 C 456 186, 468 200, 484 206" />
<path d="M634 210 C 650 200, 662 188, 678 182" />
</g>
</svg>
<div className="absolute top-[392px] left-[96px] w-[360px] rounded-xl bg-white p-4 shadow-md ring-1 ring-zinc-950/5 dark:bg-zinc-800 dark:ring-white/10">
<p className={`text-[13px] font-semibold ${ink}`}>Open questions</p>
<ul className={`mt-2 space-y-1.5 text-[13px] ${muted}`}>
<li>· Do we show pricing before the invite step?</li>
<li>· What does an empty workspace look like?</li>
</ul>
</div>
<div className="absolute top-[300px] left-[560px] flex flex-col items-start">
<svg width="18" height="18" viewBox="0 0 18 18" aria-hidden>
<path d="M2 2l13 5.5-5.5 1.6L7.8 15Z" className="fill-pink-500" />
</svg>
<span className="ml-3 rounded-full bg-pink-500 px-2 py-0.5 text-[11px] font-semibold text-white">Mei</span>
</div>
<div className="absolute top-[420px] left-[700px] flex flex-col items-start">
<svg width="18" height="18" viewBox="0 0 18 18" aria-hidden>
<path d="M2 2l13 5.5-5.5 1.6L7.8 15Z" className="fill-sky-500" />
</svg>
<span className="ml-3 rounded-full bg-sky-500 px-2 py-0.5 text-[11px] font-semibold text-white">Jonas</span>
</div>
<div className="absolute bottom-6 left-1/2 flex -translate-x-1/2 gap-1 rounded-xl bg-white p-1.5 shadow-lg ring-1 ring-zinc-950/5 dark:bg-zinc-800 dark:ring-white/10">
{["Select", "Note", "Shape", "Line", "Text"].map((tool, i) => (
<span
key={tool}
className={`rounded-lg px-3 py-1.5 text-[12px] font-medium ${i === 1 ? "bg-amber-400 text-amber-950" : muted}`}
>
{tool}
</span>
))}
</div>
</div>
);
}
const messages = [
{ who: "Priya Raman", initials: "PR", tone: "bg-violet-500", time: "9:12", text: "Build 412 is on staging. Can everyone give the new invite flow a spin before standup?" },
{ who: "Jonas Weber", initials: "JW", tone: "bg-sky-500", time: "9:20", text: "Invites from a phone work now. The email preview still shows the old logo though." },
{ who: "Mei Tanaka", initials: "MT", tone: "bg-pink-500", time: "9:24", text: "Fixed in 413, deploying now. Attaching the before and after." },
];
function PigeonApp() {
return (
<div className="flex h-full bg-white dark:bg-zinc-900">
<Side title="Northwind">
<div className="flex flex-col gap-0.5">
<p className={`px-2 pb-1 text-[11px] font-medium uppercase tracking-wide ${muted}`}>Channels</p>
<SideRow># general</SideRow>
<SideRow on># launch</SideRow>
<SideRow count={3}># design</SideRow>
<SideRow># support</SideRow>
</div>
<div className="flex flex-col gap-0.5">
<p className={`px-2 pb-1 text-[11px] font-medium uppercase tracking-wide ${muted}`}>Direct</p>
<SideRow>Priya Raman</SideRow>
<SideRow>Mei Tanaka</SideRow>
</div>
</Side>
<main className="flex flex-1 flex-col">
<header className={`flex items-center justify-between border-b px-6 py-4 ${hairline}`}>
<p className={`text-[15px] font-semibold ${ink}`}># launch</p>
<p className={`text-[12px] ${muted}`}>14 members</p>
</header>
<div className="flex flex-1 flex-col gap-5 px-6 py-5">
{messages.map((message) => (
<div key={message.time} className="flex gap-3">
<Face initials={message.initials} className={`size-9 rounded-lg text-[12px] ring-0 ${message.tone}`} />
<div className="min-w-0">
<p className="text-[13px]">
<span className={`font-semibold ${ink}`}>{message.who}</span>
<span className={`ml-2 text-[11.5px] ${muted}`}>{message.time}</span>
</p>
<p className={`mt-0.5 text-[14px] leading-relaxed ${ink}`}>{message.text}</p>
</div>
</div>
))}
<div className={`ml-12 flex w-[300px] items-center gap-3 rounded-lg border p-2.5 ${hairline}`}>
<span className="grid size-10 place-items-center rounded-md bg-sky-500/15 text-[11px] font-semibold text-sky-700 dark:text-sky-300">
PNG
</span>
<div>
<p className={`text-[13px] font-medium ${ink}`}>invite-email-before-after.png</p>
<p className={`text-[11.5px] ${muted}`}>418 KB</p>
</div>
</div>
</div>
<div className="px-6 pb-5">
<p className={`mb-2 text-[12px] ${muted}`}>Priya is typing…</p>
<div className={`rounded-xl border px-4 py-3 text-[13.5px] ${hairline} ${muted}`}>Message # launch</div>
</div>
</main>
</div>
);
}
/* ── Demo ──────────────────────────────────────────────── */
const apps: (LaunchDockItem & { line: string })[] = [
{ id: "plume", label: "Plume", title: "Field notes: Lisbon offsite", icon: icons.plume, content: <PlumeApp />, line: "Docs that research teams write together, offline on the train." },
{ id: "cadence", label: "Cadence", title: "Cadence: week 46", icon: icons.cadence, content: <CadenceApp />, line: "A shared calendar where a moved meeting moves for everyone at once." },
{ id: "tally", label: "Tally", title: "Tally: November budget", icon: icons.tally, content: <TallyApp />, line: "A household budget that two phones and a laptop keep honest." },
{ id: "loop", label: "Loop", title: "Loop", icon: icons.loop, content: <LoopApp />, line: "A music library that picks up on the laptop where the phone stopped." },
{ id: "tessera", label: "Tessera", title: "Onboarding flow v2: Tessera", icon: icons.tessera, content: <TesseraApp />, line: "A whiteboard where every sticky lands for everyone in the room." },
{ id: "pigeon", label: "Pigeon", title: "# launch: Northwind", icon: icons.pigeon, content: <PigeonApp />, line: "Team chat that never shows a message twice, even after a dead zone." },
];
export function LaunchDockDemo() {
const [open, setOpen] = React.useState(apps[0].id);
const app = apps.find((item) => item.id === open) ?? apps[0];
return (
<div className="flex w-full max-w-3xl flex-col items-center gap-5">
<LaunchDock items={apps} value={open} onValueChange={setOpen} autoplay dockLabel="Apps built on Shoal" />
<p aria-live="polite" className="max-w-md text-center text-sm text-balance text-zinc-500 dark:text-zinc-400">
<span className="font-medium text-zinc-900 dark:text-zinc-100">{app.label}</span> · {app.line}
</p>
</div>
);
}
Live preview, examples and props: https://keyframeui.com/components/launch-dock#launch-dock-demo