Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
213 changes: 110 additions & 103 deletions bun.lock

Large diffs are not rendered by default.

881 changes: 881 additions & 0 deletions components/motion/morphing-tabs.tsx

Large diffs are not rendered by default.

91 changes: 91 additions & 0 deletions components/previews/blocks/morphing-tabs.preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"use client";

import { useMemo, useState } from "react";
import { MorphingTabs, type MorphingTabsItem } from "@/components/motion/morphing-tabs";

const ROOM_CONTENT: Record<string, { eyebrow: string; title: string; detail: string; accent: string }> = {
"room-2": {
eyebrow: "quiet focus",
title: "Room 2",
detail: "A small space for the work that needs a little more air around it.",
accent: "#db5b2f",
},
general: {
eyebrow: "shared space",
title: "General",
detail: "The common room for notes, links and the ideas that are still finding their shape.",
accent: "#1bb273",
},
archive: {
eyebrow: "kept close",
title: "Archive",
detail: "Past rooms stay available without competing with the conversations in motion.",
accent: "#7a6de2",
},
};

function RoomPanel({ id }: { id: string }) {
const room = ROOM_CONTENT[id];

return (
<div className="relative min-h-64 overflow-hidden bg-[radial-gradient(circle_at_1px_1px,#dfe2e3_1px,transparent_1.5px)] bg-[size:4.8rem_4.8rem] px-7 py-8 md:px-12 md:py-10">
<div className="relative max-w-xl">
<p className="text-[0.68rem] font-semibold uppercase tracking-[0.24em] text-black/40">
{room.eyebrow}
</p>
<h3 className="mt-3 text-3xl font-light tracking-[-0.055em] text-[#151515] md:text-5xl">
{room.title}
</h3>
<p className="mt-4 max-w-md text-sm leading-6 text-black/55 md:text-base">
{room.detail}
</p>
<div className="mt-8 flex items-center gap-3 text-xs font-medium text-black/50">
<span className="size-2 rounded-full" style={{ backgroundColor: room.accent }} />
drag any room to reorder
</div>
</div>
</div>
);
}

export function MorphingTabsPreview() {
const initialItems = useMemo<MorphingTabsItem[]>(
() =>
Object.keys(ROOM_CONTENT).map((id) => ({
id,
label: ROOM_CONTENT[id].title,
content: <RoomPanel id={id} />,
})),
[],
);
const [items, setItems] = useState(initialItems);
const [value, setValue] = useState<string | null>("room-2");

return (
<div className="flex w-full min-w-0 items-center justify-center bg-[#242424] p-3 md:p-8">
<MorphingTabs
items={items}
value={value}
onValueChange={setValue}
onOrderChange={(ids) => {
setItems((current) => {
const byId = new Map(current.map((item) => [item.id, item]));
return ids.flatMap((id) => {
const item = byId.get(id);
return item ? [item] : [];
});
});
}}
onClose={(id) => {
setItems((current) => {
const next = current.filter((item) => item.id !== id);
if (id === value) setValue(next[0]?.id ?? null);
return next;
});
}}
ariaLabel="Rooms"
className="w-full max-w-5xl"
/>
</div>
);
}
3 changes: 3 additions & 0 deletions components/previews/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,9 @@ export const previews: Record<string, ComponentType> = {
"blocks/expandable-tabs": dynamic(() =>
import("./blocks/expandable-tabs.preview").then((m) => m.ExpandableTabsPreview),
),
"blocks/morphing-tabs": dynamic(() =>
import("./blocks/morphing-tabs.preview").then((m) => m.MorphingTabsPreview),
),
"blocks/swipeable-list": dynamic(() =>
import("./blocks/swipeable-list.preview").then((m) => m.SwipeableListPreview),
),
Expand Down
1 change: 1 addition & 0 deletions lib/component-dates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ const COMPONENT_DATES = {
"blocks/expandable-action-bar": { publishedAt: "2026-06-05", updatedAt: "2026-06-22" },
"blocks/overflow-actions": { publishedAt: "2026-06-19", updatedAt: "2026-06-28" },
"blocks/expandable-tabs": { publishedAt: "2026-06-14", updatedAt: "2026-06-28" },
"blocks/morphing-tabs": { publishedAt: "2026-08-06", updatedAt: "2026-08-06" },
"blocks/swipeable-list": { publishedAt: "2026-06-15", updatedAt: "2026-06-28" },
"blocks/file-upload": { publishedAt: "2026-06-18", updatedAt: "2026-07-30" },
"blocks/prediction-market": { publishedAt: "2026-06-18", updatedAt: "2026-06-21" },
Expand Down
15 changes: 15 additions & 0 deletions lib/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1467,6 +1467,21 @@ export const registry: CategoryEntry[] = [
description: "Icon tab bar where the active tab expands to a labelled pill, with a panel above that morphs height and slides content direction-aware on switch.",
file: "components/motion/expandable-tabs.tsx",
},
{
slug: "morphing-tabs",
name: "Morphing Tabs",
description: "Reorderable tabs whose selected item grows into a white content surface, with the active shape gliding as tabs move and a shared morph between rooms.",
file: "components/motion/morphing-tabs.tsx",
badge: "new",
launchedAt: "2026-08-06",
keywords: [
"reorderable tabs react",
"drag tabs react",
"morphing tabs component",
"animated tab panel",
"shared layout tabs",
],
},
{
slug: "swipeable-list",
name: "Swipeable List",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"lenis": "^1.3.23",
"lucide-react": "^1.16.0",
"motion": "^11.15.0",
"next": "^15.1.4",
"next": "16.3.0",
"next-themes": "^0.4.6",
"react": "^19.0.0",
"react-dom": "^19.0.0",
Expand Down
4 changes: 2 additions & 2 deletions middleware.ts → proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { type NextFetchEvent, type NextRequest, NextResponse } from "next/server
/**
* Registry installs happen via the shadcn CLI fetching `/r/{slug}.json`. That
* request is headless (no browser, no gtag), and the route is force-static so
* no per-request handler runs. Middleware runs on every request even for
* no per-request handler runs. Proxy runs on every matching request even for
* static assets, so we tag installs here and report them to GA4 server-side
* via the Measurement Protocol, keeping the route on the CDN.
*/
Expand All @@ -12,7 +12,7 @@ export const config = { matcher: "/r/:path*" };
const GA_ID = process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS_ID;
const GA_SECRET = process.env.GA_API_SECRET;

export function middleware(req: NextRequest, event: NextFetchEvent) {
export function proxy(req: NextRequest, event: NextFetchEvent) {
const pass = NextResponse.next();

const { pathname } = req.nextUrl;
Expand Down
5 changes: 3 additions & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
Expand All @@ -33,7 +33,8 @@
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"build/types/**/*.ts"
"build/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"examples/shadcn-vite-install",
Expand Down
Loading