{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dynamic-island",
  "type": "registry:ui",
  "title": "Dynamic Island",
  "description": "A pill that morphs between idle, compact and expanded live activities on a spring.",
  "author": "WANSTUDIO <https://wanstudio.my>",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/wancraft/dynamic-island.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\n\nexport interface DynamicIslandView {\n  /** Short name of the activity, e.g. \"Now playing\". Used in announcements and the expand button. */\n  label: string;\n  /** Content of the compact pill. Keep it non-interactive: the whole pill is the expand button. */\n  compact: React.ReactNode;\n  /** Content of the expanded card. May contain controls. */\n  expanded: React.ReactNode;\n  /** Optional polite announcement when this activity starts (defaults to the label). */\n  announcement?: string;\n}\n\ntype Spring = { stiffness: number; damping: number; mass: number };\n\nexport interface DynamicIslandProps<K extends string = string> extends Omit<React.HTMLAttributes<HTMLDivElement>, \"children\"> {\n  /** Every activity the island can show, keyed by name. */\n  views: Record<K, DynamicIslandView>;\n  /** The current activity, or null for the idle pill. */\n  activity: K | null;\n  /** Expanded state (controlled). */\n  expanded?: boolean;\n  /** Initial expanded state (uncontrolled). Resets to collapsed when the activity changes. */\n  defaultExpanded?: boolean;\n  onExpandedChange?: (expanded: boolean) => void;\n  /** Spring used to morph the size and shape. */\n  spring?: Spring;\n  /** Width of the idle pill in px. */\n  idleWidth?: number;\n  /** Width of the expanded card in px. */\n  expandedWidth?: number;\n  /** Blur content as it cross-fades between states. */\n  blur?: boolean;\n  /** Accent for the focus ring, also exposed to views as the CSS variable --di-accent. */\n  accent?: string;\n}\n\nconst IDLE_HEIGHT = 36;\n\n/**\n * A black pill that morphs between idle, compact and expanded states on a spring, measuring each\n * state's content so it always lands on the right size. Content cross-fades between states and\n * every change is announced through a polite live region.\n */\nexport function DynamicIsland<K extends string = string>({\n  views,\n  activity,\n  expanded,\n  defaultExpanded = false,\n  onExpandedChange,\n  spring = { stiffness: 400, damping: 30, mass: 0.9 },\n  idleWidth = 120,\n  expandedWidth = 340,\n  blur = true,\n  accent = \"#FF7448\",\n  className = \"\",\n  style,\n  ...props\n}: DynamicIslandProps<K>) {\n  const uid = React.useId();\n  const reduceMotion = useReducedMotion();\n  const rootRef = React.useRef<HTMLDivElement>(null);\n  const expandButtonRef = React.useRef<HTMLButtonElement>(null);\n  const panelRef = React.useRef<HTMLDivElement>(null);\n  /** Move focus after the next state change (only for user-initiated changes). */\n  const focusIntentRef = React.useRef<\"panel\" | \"button\" | null>(null);\n\n  const [internalExpanded, setInternalExpanded] = React.useState(defaultExpanded);\n  // A new activity always starts collapsed (uncontrolled mode).\n  const [prevActivity, setPrevActivity] = React.useState(activity);\n  if (prevActivity !== activity) {\n    setPrevActivity(activity);\n    setInternalExpanded(false);\n  }\n  const isExpanded = expanded ?? internalExpanded;\n  const view = activity === null ? null : views[activity];\n  const mode: \"idle\" | \"compact\" | \"expanded\" = !view ? \"idle\" : isExpanded ? \"expanded\" : \"compact\";\n  const contentKey = view ? `${activity}-${mode}` : \"idle\";\n\n  // Size of the content currently on screen, measured with a ResizeObserver.\n  const [size, setSize] = React.useState({ width: idleWidth, height: IDLE_HEIGHT });\n\n  const setExpanded = (next: boolean, focus: \"panel\" | \"button\" | null) => {\n    if (next === isExpanded) return;\n    focusIntentRef.current = focus;\n    if (expanded === undefined) setInternalExpanded(next);\n    onExpandedChange?.(next);\n  };\n\n  React.useEffect(() => {\n    const intent = focusIntentRef.current;\n    focusIntentRef.current = null;\n    if (intent === \"panel\") panelRef.current?.focus({ preventScroll: true });\n    if (intent === \"button\") expandButtonRef.current?.focus({ preventScroll: true });\n  }, [mode]);\n\n  const outsidePointer = React.useEffectEvent((event: PointerEvent) => {\n    if (!rootRef.current?.contains(event.target as Node)) setExpanded(false, null);\n  });\n\n  // Pressing outside the expanded island collapses it.\n  React.useEffect(() => {\n    if (mode !== \"expanded\") return;\n    const onDown = (event: PointerEvent) => outsidePointer(event);\n    document.addEventListener(\"pointerdown\", onDown);\n    return () => document.removeEventListener(\"pointerdown\", onDown);\n  }, [mode]);\n\n  const measure = (node: HTMLDivElement | null) => {\n    if (!node) return;\n    const observer = new ResizeObserver(() => {\n      const width = node.offsetWidth;\n      const height = node.offsetHeight;\n      setSize((prev) => (prev.width === width && prev.height === height ? prev : { width, height }));\n    });\n    observer.observe(node);\n    return () => observer.disconnect();\n  };\n\n  const morph = reduceMotion ? { duration: 0 } : { type: \"spring\" as const, ...spring };\n  const fade = reduceMotion ? { duration: 0 } : { duration: 0.22, ease: [0.16, 1, 0.3, 1] as const };\n  const hidden = reduceMotion\n    ? { opacity: 0 }\n    : { opacity: 0, scale: 0.92, filter: blur ? \"blur(6px)\" : \"blur(0px)\" };\n  const shown = reduceMotion ? { opacity: 1 } : { opacity: 1, scale: 1, filter: \"blur(0px)\" };\n\n  const radius = mode === \"expanded\" ? Math.min(32, size.height / 2) : size.height / 2;\n  const announcement = view ? (mode === \"expanded\" ? `${view.label}, expanded` : (view.announcement ?? view.label)) : \"\";\n\n  return (\n    <div\n      ref={rootRef}\n      className={`relative inline-flex justify-center ${className}`}\n      style={{ [\"--di-accent\" as string]: accent, ...style }}\n      onKeyDown={(event) => {\n        if (event.key === \"Escape\" && mode === \"expanded\") {\n          event.preventDefault();\n          setExpanded(false, \"button\");\n        }\n      }}\n      {...props}\n    >\n      <motion.div\n        initial={false}\n        animate={{ width: size.width, height: size.height, borderRadius: radius }}\n        transition={morph}\n        aria-hidden={mode === \"idle\" || undefined}\n        className=\"relative grid place-items-center overflow-hidden bg-black text-white shadow-[0_12px_32px_-14px_rgba(0,0,0,0.55)] dark:ring-1 dark:ring-white/[0.08]\"\n      >\n        <AnimatePresence mode=\"popLayout\" initial={false}>\n          <motion.div\n            key={contentKey}\n            initial={hidden}\n            animate={shown}\n            exit={hidden}\n            transition={fade}\n            className=\"col-start-1 row-start-1\"\n          >\n            <div ref={measure}>\n              {mode === \"idle\" && <div style={{ width: idleWidth, height: IDLE_HEIGHT }} />}\n              {mode === \"compact\" && view && (\n                <div\n                  id={`${uid}-compact`}\n                  aria-hidden=\"true\"\n                  className=\"flex h-9 items-center justify-between gap-4 px-3.5 text-[13px] font-medium tracking-tight\"\n                  style={{ minWidth: idleWidth }}\n                >\n                  {view.compact}\n                </div>\n              )}\n              {mode === \"expanded\" && view && (\n                <div\n                  ref={panelRef}\n                  role=\"group\"\n                  aria-label={view.label}\n                  tabIndex={-1}\n                  className=\"relative p-4 outline-none\"\n                  // Never wider than a small viewport.\n                  style={{ width: expandedWidth, maxWidth: \"calc(100vw - 32px)\" }}\n                >\n                  <button\n                    type=\"button\"\n                    onClick={() => setExpanded(false, \"button\")}\n                    className=\"sr-only rounded-full px-2 py-0.5 text-[11px] focus-visible:not-sr-only focus-visible:absolute focus-visible:right-3 focus-visible:top-3 focus-visible:z-10 focus-visible:bg-white/15 focus-visible:outline-2 focus-visible:outline-[var(--di-accent)]\"\n                  >\n                    Collapse\n                  </button>\n                  {view.expanded}\n                </div>\n              )}\n            </div>\n          </motion.div>\n        </AnimatePresence>\n\n        {mode === \"compact\" && view && (\n          // The whole compact pill is the expand control; its content describes it.\n          <button\n            ref={expandButtonRef}\n            type=\"button\"\n            aria-expanded={false}\n            aria-label={`${view.label}: expand`}\n            aria-describedby={`${uid}-compact`}\n            onClick={() => setExpanded(true, \"panel\")}\n            className=\"absolute inset-0 rounded-[inherit] outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[var(--di-accent)]\"\n          />\n        )}\n      </motion.div>\n\n      <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n        {announcement}\n      </span>\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "navigation"
  ],
  "docs": "Documentation: https://wancraft.my/components/dynamic-island"
}
