(null);
useLiquidGlassTabs(track, { appearance: "class" });
const index = options.indexOf(value);
return (
{
const step = event.key === "ArrowRight" ? 1 : event.key === "ArrowLeft" ? -1 : 0;
if (!step) return;
event.preventDefault();
const next = options[(index + step + options.length) % options.length];
if (next === undefined) return;
onChange(next);
track.current?.querySelector(`[data-tab="${next}"]`)?.focus();
}}
>
{options.map((option) => (
onChange(option)}
className="rounded-full px-3.5 py-1.5 text-sm font-medium whitespace-nowrap text-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring sm:px-4"
>
{option}
))}
);
}
// ── Search field ────────────────────────────────────────
export function GlassSearch({ className, placeholder = "Search" }: { className?: string; placeholder?: string }) {
const [value, setValue] = React.useState("");
return (
setValue(event.target.value)}
placeholder={placeholder}
aria-label={placeholder}
className="min-w-0 flex-1 bg-transparent text-sm outline-none placeholder:text-foreground/55"
/>
⌘K
);
}
// ── Toolbar ─────────────────────────────────────────────
const TOOLS = [
{ id: "bold", label: "Bold", icon: IconBold },
{ id: "italic", label: "Italic", icon: IconItalic },
{ id: "strike", label: "Strikethrough", icon: IconStrikethrough },
{ id: "link", label: "Link", icon: IconLink },
{ id: "list", label: "List", icon: IconList },
] as const;
export function GlassToolbar({ className }: { className?: string }) {
const [on, setOn] = React.useState>(() => new Set(["bold"]));
return (
{TOOLS.map(({ id, label, icon: Icon }) => {
const active = on.has(id);
return (
setOn((current) => {
const next = new Set(current);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
})
}
className={cn(
"grid size-9 place-items-center rounded-full text-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring",
active ? "liquid-glass-well" : "text-foreground/70 hover:text-foreground",
)}
>
);
})}
);
}
// ── Menu ────────────────────────────────────────────────
const SORTS = ["Date added", "Name", "Size", "Kind"];
/**
* Glass menu, as iOS 27's: it pours out of its button and covers it, the button
* becoming the menu, and closing it draws back into the button as a drop.
*/
export function GlassMenu({ className }: { className?: string }) {
const [open, setOpen] = React.useState(false);
const [sort, setSort] = React.useState(SORTS[0]!);
const root = React.useRef(null);
const button = React.useRef(null);
const glass = React.useRef(null);
// While it draws back, pressing the button pours it straight back out.
const closing = React.useRef(false);
const close = React.useCallback(() => {
if (closing.current) return;
closing.current = true;
void glass.current?.drawBack().then((gone) => {
if (!gone || !closing.current) return;
closing.current = false;
setOpen(false);
});
}, []);
const reopen = () => {
closing.current = false;
if (button.current) void glass.current?.pourFrom(button.current, { cover: true });
};
React.useEffect(() => {
if (!open) return;
const onDown = (event: PointerEvent) => {
if (!root.current?.contains(event.target as Node)) close();
};
const onKey = (event: KeyboardEvent) => {
if (event.key === "Escape") close();
};
window.addEventListener("pointerdown", onDown);
window.addEventListener("keydown", onKey);
return () => {
window.removeEventListener("pointerdown", onDown);
window.removeEventListener("keydown", onKey);
};
}, [open, close]);
return (
(!open ? setOpen(true) : closing.current ? reopen() : close())}
className="relative flex h-10 items-center gap-1.5 rounded-full pr-3 pl-4 text-sm font-medium text-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
Sort by {sort.toLowerCase()}
{open ? (
{
setSort(item);
close();
}}
/>
) : null}
);
}
function SortMenu({
button,
glass,
sort,
onChoose,
}: {
button: React.RefObject;
glass: React.RefObject;
sort: string;
onChoose: (item: string) => void;
}) {
const menu = React.useRef(null);
// A pill under the row you point at, stretching toward the next one.
useLiquidGlassHighlight(menu);
// Placed below its button, it pours out of it and over it.
React.useLayoutEffect(() => {
if (button.current) void glass.current?.pourFrom(button.current, { cover: true });
}, [button, glass]);
return (
{SORTS.map((item) => (
onChoose(item)}
className="flex w-full items-center justify-between rounded-2xl px-3 py-2 text-left text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
{item}
{item === sort ? : null}
))}
);
}
// ── Dialog ──────────────────────────────────────────────
/** A glass dialog inside its stage: floating="dialog", materializing as it zooms in. */
export function GlassDialog({ className }: { className?: string }) {
const [open, setOpen] = React.useState(false);
const [leaving, setLeaving] = React.useState(false);
const [hover, setHover] = React.useState(false);
const glass = React.useRef(null);
const trigger = React.useRef(null);
const first = React.useRef(null);
const close = React.useCallback(() => {
setLeaving(true);
void glass.current?.dematerialize({ scale: 0.98 }).then(() => {
setOpen(false);
setLeaving(false);
trigger.current?.focus();
});
}, []);
React.useLayoutEffect(() => {
if (!open || leaving) return;
void glass.current?.materialize({ scale: 0.965 });
first.current?.focus();
}, [open, leaving]);
return (
setOpen(true)}
className="relative flex h-10 items-center rounded-full px-5 text-sm font-medium text-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
Share album…
<>
{open ? (
event.key === "Escape" && close()}>
Share “Summer 2026”
Anyone with the link can see the 48 photos in this album.
setHover(true)}
onPointerLeave={() => setHover(false)}
className={cn("h-10 rounded-full text-sm font-medium outline-none focus-visible:ring-2 focus-visible:ring-ring", hover && "liquid-glass-well")}
>
Cancel
Copy link
) : null}
>
);
}
// ── Card ────────────────────────────────────────────────
/** Regular glass over text: drag it and the rim bends the letters it crosses. */
export function GlassCard({ initial = { x: 0.5, y: 0.5 } }: { initial?: { x: number; y: number } }) {
const drag = useDraggable({ initial });
return (
Drag me over the text
The rim bends each letter as it crosses.
);
}