{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"dynamic-island","type":"registry:component","title":"Dynamic Island","description":"iOS-style island pill that morphs between live activity views with bouncy shell resize and blur crossfades.","author":"Saurabh <saurabh10102@gmail.com>","dependencies":["clsx","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/dynamic-island.tsx","type":"registry:component","target":"@components/motion/dynamic-island.tsx","content":"\"use client\";\n\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport {\n  createContext,\n  useContext,\n  useEffect,\n  useRef,\n  useState,\n  type ReactNode,\n} from \"react\";\nimport { EASE_OUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\ntype IslandContextValue = {\n  view: string | null;\n};\n\nconst IslandContext = createContext<IslandContextValue | null>(null);\n\n// Shell physics, Apple style: expansion blooms out of the pill with a visible\n// overshoot, and collapse returns with the same life — the pill squeezes a\n// touch past its size and springs back. The shell animates real width/height\n// (not transforms), so slots are never scale-distorted.\nconst EXPAND_SPRING = {\n  type: \"spring\",\n  stiffness: 550,\n  damping: 25,\n  mass: 0.5,\n} as const;\n\nconst COLLAPSE_SPRING = {\n  type: \"spring\",\n  stiffness: 520,\n  damping: 24,\n  mass: 0.45,\n} as const;\n\n// Content pops from the pill core just after the shell starts moving.\nconst CONTENT_SPRING = {\n  type: \"spring\",\n  stiffness: 560,\n  damping: 28,\n  mass: 0.5,\n} as const;\n\n// Real radii, tweened separately from the size spring. Springing between a\n// fake huge radius and a small one makes corners glitch mid-resize; these two\n// values are close (18.5 is exactly half the 37px pill) so the corner shape\n// stays stable throughout.\nconst RADIUS_COMPACT = 18.5;\nconst RADIUS_EXPANDED = 24;\n\n/** Tracks the natural size of the content so the shell can spring to it. */\nfunction useContentSize() {\n  const ref = useRef<HTMLDivElement | null>(null);\n  const [size, setSize] = useState<{ width: number; height: number } | null>(null);\n\n  useEffect(() => {\n    const el = ref.current;\n    if (!el || typeof ResizeObserver === \"undefined\") return;\n    const observer = new ResizeObserver(() => {\n      setSize({ width: el.offsetWidth, height: el.offsetHeight });\n    });\n    observer.observe(el);\n    return () => observer.disconnect();\n  }, []);\n\n  return [ref, size] as const;\n}\n\nfunction Slot({\n  keyId,\n  children,\n  className,\n  scaleFrom = 0.8,\n  delay = 0.04,\n}: {\n  keyId: string;\n  children: ReactNode;\n  className?: string;\n  /** Scale the content emerges from — everything originates at the pill. */\n  scaleFrom?: number;\n  /** Lets the shell lead the bloom before content appears. */\n  delay?: number;\n}) {\n  const reduce = useReducedMotion();\n  return (\n    <motion.div\n      key={keyId}\n      initial={\n        reduce\n          ? { opacity: 0 }\n          : { opacity: 0, scale: scaleFrom, y: -8, filter: \"blur(8px)\" }\n      }\n      animate={\n        reduce\n          ? { opacity: 1 }\n          : { opacity: 1, scale: 1, y: 0, filter: \"blur(0px)\" }\n      }\n      // Exit gets sucked up into the pill — fast, blur-free, before the\n      // shrinking shell can clip it.\n      exit={\n        reduce\n          ? { opacity: 0, transition: { duration: 0.1 } }\n          : {\n              opacity: 0,\n              scale: 0.85,\n              y: -6,\n              transition: { duration: 0.08, ease: EASE_OUT },\n            }\n      }\n      transition={\n        reduce\n          ? { duration: 0.15 }\n          : {\n              ...CONTENT_SPRING,\n              delay,\n              opacity: { duration: 0.18, ease: EASE_OUT, delay },\n              filter: { duration: 0.22, ease: EASE_OUT, delay },\n            }\n      }\n      // Anchored to the pill line: content unfurls downward out of it and is\n      // sucked back up into it.\n      style={{ transformOrigin: \"top center\" }}\n      className={cn(\"flex items-center justify-center\", className)}\n    >\n      {children}\n    </motion.div>\n  );\n}\n\nexport interface DynamicIslandProps {\n  /** Active view id. `null` shows the compact pill. */\n  view: string | null;\n  /** Compact pill content, shown when no view is active. */\n  compact?: ReactNode;\n  /** DynamicIslandView elements. */\n  children?: ReactNode;\n  className?: string;\n}\n\nexport function DynamicIsland({\n  view,\n  compact,\n  children,\n  className,\n}: DynamicIslandProps) {\n  const reduce = useReducedMotion();\n  const expanded = view !== null;\n  const [sizerRef, size] = useContentSize();\n\n  return (\n    <IslandContext.Provider value={{ view }}>\n      <motion.div\n        role=\"status\"\n        aria-live=\"polite\"\n        initial={false}\n        animate={\n          size\n            ? {\n                width: size.width,\n                height: size.height,\n                borderRadius: expanded ? RADIUS_EXPANDED : RADIUS_COMPACT,\n              }\n            : { borderRadius: expanded ? RADIUS_EXPANDED : RADIUS_COMPACT }\n        }\n        transition={\n          reduce\n            ? { duration: 0 }\n            : {\n                ...(expanded ? EXPAND_SPRING : COLLAPSE_SPRING),\n                borderRadius: { duration: 0.2, ease: EASE_OUT },\n              }\n        }\n        // items-start pins content to the top edge while the shell springs, so\n        // expansion reads as unfurling downward out of the pill. Top-align the\n        // island in its parent (like under a notch) to complete the effect.\n        className={cn(\n          \"relative inline-flex items-start justify-center overflow-hidden\",\n          \"bg-foreground text-background shadow-2xl\",\n          className,\n        )}\n      >\n        {/* w-max keeps this at the natural size of the active content; the\n            shell springs toward it. */}\n        <div ref={sizerRef} className=\"w-max\">\n          <AnimatePresence mode=\"popLayout\" initial={false}>\n            {!expanded && compact ? (\n              <Slot\n                keyId=\"compact\"\n                scaleFrom={0.85}\n                delay={0.06}\n                // iPhone pill proportions: ~126 x 37.\n                className=\"min-h-[37px] min-w-[126px] gap-2 px-4 py-1.5 text-xs font-medium\"\n              >\n                {compact}\n              </Slot>\n            ) : null}\n          </AnimatePresence>\n          {children}\n        </div>\n      </motion.div>\n    </IslandContext.Provider>\n  );\n}\n\nexport interface DynamicIslandViewProps {\n  /** Matches the parent `view` prop when active. */\n  id: string;\n  children: ReactNode;\n  className?: string;\n}\n\nexport function DynamicIslandView({ id, children, className }: DynamicIslandViewProps) {\n  const ctx = useContext(IslandContext);\n  if (!ctx) throw new Error(\"DynamicIslandView must be used inside <DynamicIsland>\");\n  const active = ctx.view === id;\n\n  return (\n    <AnimatePresence mode=\"popLayout\" initial={false}>\n      {active ? (\n        <Slot keyId={id} className={cn(\"px-6 py-4\", className)}>\n          {children}\n        </Slot>\n      ) : null}\n    </AnimatePresence>\n  );\n}\n"},{"path":"lib/ease.ts","type":"registry:lib","target":"@lib/ease.ts","content":"// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"}]}