Dynamic-Island header
A pill that morphs into a card on one shared layout animation, not a cross-fade
npx shadcn@latest add https://ethankd.com/r/island.json"use client";
/**
* A Dynamic-Island-style floating header: pill collapsed, card expanded,
* one `layout` animation morphing width/height/radius together so it
* genuinely reshapes rather than cross-fading between two boxes. Springs
* (not CSS transitions) so a tap mid-flight reverses smoothly instead of
* jumping, critically damped since this is a tap, not a flick.
*/
import { AnimatePresence, MotionConfig, motion, useReducedMotion } from "motion/react";
const SPRING = { type: "spring" as const, bounce: 0, duration: 0.42 };
const CONTENT_SPRING = { type: "spring" as const, bounce: 0, duration: 0.32 };
export function Island({
open,
onToggle,
collapsedMetric,
collapsedLabel,
expanded,
}: {
open: boolean;
onToggle: () => void;
collapsedMetric: string;
collapsedLabel: string;
expanded: React.ReactNode;
}) {
const reduced = useReducedMotion();
return (
<MotionConfig reducedMotion="user">
<motion.div
layout
transition={reduced ? { duration: 0.12 } : SPRING}
style={{ transformOrigin: "top center", willChange: "transform" }}
className={`pointer-events-auto mx-auto overflow-hidden border border-white/12 bg-white/[0.07] shadow-[0_20px_60px_-20px_rgba(0,0,0,0.95)] backdrop-blur-2xl ${
open ? "w-full max-w-md rounded-[1.75rem] p-1.5" : "w-fit rounded-full p-1"
}`}
>
<motion.div
layout
transition={reduced ? { duration: 0.12 } : SPRING}
className={`bg-black/45 shadow-[inset_0_1px_1px_rgba(255,255,255,0.09)] ${
open ? "rounded-[calc(1.75rem-0.375rem)] p-4" : "rounded-full"
}`}
>
<AnimatePresence mode="popLayout" initial={false}>
{open ? (
<motion.div
key="expanded"
layout
initial={{ opacity: 0, filter: "blur(8px)", scale: 0.97 }}
animate={{ opacity: 1, filter: "blur(0px)", scale: 1 }}
exit={{ opacity: 0, filter: "blur(8px)", scale: 0.97 }}
transition={reduced ? { duration: 0.12 } : CONTENT_SPRING}
>
{expanded}
</motion.div>
) : (
<motion.button
key="collapsed"
layout
onClick={onToggle}
initial={{ opacity: 0, filter: "blur(8px)", scale: 0.97 }}
animate={{ opacity: 1, filter: "blur(0px)", scale: 1 }}
exit={{ opacity: 0, filter: "blur(8px)", scale: 0.97 }}
transition={reduced ? { duration: 0.12 } : CONTENT_SPRING}
whileTap={{ scale: 0.97 }}
aria-expanded={false}
aria-label={`${collapsedMetric} ${collapsedLabel}. Expand panel`}
className="flex h-12 items-center gap-2.5 whitespace-nowrap rounded-full pl-4 pr-3.5"
>
<span className="h-2 w-2 shrink-0 rounded-full bg-[#22c98a]" style={{ boxShadow: "0 0 10px #22c98a" }} />
<span className="text-[15px] font-semibold tabular-nums text-white">{collapsedMetric}</span>
<span className="text-[12px] text-white/65">{collapsedLabel}</span>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" className="ml-0.5 shrink-0 text-white/60" aria-hidden>
<path d="M2.5 4.5L6 8l3.5-3.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</motion.button>
)}
</AnimatePresence>
</motion.div>
</motion.div>
</MotionConfig>
);
}
/** Collapse affordance for the expanded state — mirrors the chevron on the pill. */
export function IslandCollapse({ onClick, label }: { onClick: () => void; label: string }) {
return (
<motion.button
onClick={onClick}
whileTap={{ scale: 0.94 }}
aria-label={label}
aria-expanded
className="flex h-9 w-9 items-center justify-center rounded-full border border-white/10 bg-white/5 text-white/60"
>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden>
<path d="M2.5 7.5L6 4l3.5 3.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</motion.button>
);
}
The preview wires Island and IslandCollapse into a small stateful demo; the code tab shows the component you install.