{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "dynamic-island", "title": "Dynamic Island", "description": "A functional iPhone-inspired island that morphs between compact and expanded content.", "dependencies": [ "framer-motion" ], "registryDependencies": [], "files": [ { "path": "registry/spark-ui/dynamic-island.tsx", "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { AnimatePresence, motion, useReducedMotion } from \"framer-motion\";\nimport * as React from \"react\";\n\nexport interface DynamicIslandProps {\n compact?: React.ReactNode;\n expanded?: React.ReactNode;\n open?: boolean;\n defaultOpen?: boolean;\n onOpenChange?: (open: boolean) => void;\n className?: string;\n expandLabel?: string;\n collapseLabel?: string;\n}\n\nexport function DynamicIsland({\n compact,\n expanded,\n open,\n defaultOpen = false,\n onOpenChange,\n className,\n expandLabel = \"Expand Dynamic Island\",\n collapseLabel = \"Collapse Dynamic Island\",\n}: DynamicIslandProps) {\n const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen);\n const [isRejecting, setIsRejecting] = React.useState(false);\n const restoreFocus = React.useRef(false);\n const reducedMotion = useReducedMotion();\n const isOpen = open ?? uncontrolledOpen;\n const isPanelOpen = isOpen && expanded != null;\n const isEmpty = compact == null && expanded == null;\n const isPillOpen = isOpen && expanded == null && !isEmpty;\n\n const triggerRef = React.useCallback((node: HTMLButtonElement | null) => {\n if (node && restoreFocus.current) {\n node.focus();\n restoreFocus.current = false;\n }\n }, []);\n\n const setOpen = (nextOpen: boolean) => {\n if (open === undefined) setUncontrolledOpen(nextOpen);\n onOpenChange?.(nextOpen);\n };\n\n const collapse = () => {\n restoreFocus.current = true;\n setOpen(false);\n };\n\n return (\n {\n if (isRejecting) setIsRejecting(false);\n }}\n onKeyDown={(event) => {\n if (event.key === \"Escape\" && isOpen) {\n event.preventDefault();\n if (isPanelOpen) collapse();\n else setOpen(false);\n }\n }}\n transition={\n reducedMotion\n ? { duration: 0 }\n : {\n layout: { type: \"spring\", stiffness: 420, damping: 34 },\n x: { delay: 0.3, duration: 0.38, ease: \"easeInOut\" },\n }\n }\n className={cn(\n \"relative isolate overflow-hidden border border-white/15 bg-black text-white shadow-[0_14px_50px_rgba(0,0,0,0.28)]\",\n isPanelOpen\n ? \"h-48 w-[min(22rem,calc(100vw-3rem))] rounded-[2rem]\"\n : isRejecting || isPillOpen\n ? \"h-16 w-52 rounded-full\"\n : \"h-11 w-32 rounded-full\",\n className,\n )}\n >\n \n {isPanelOpen ? (\n \n {expanded}\n \n ) : (\n {\n if (isEmpty) {\n if (!reducedMotion) setIsRejecting(true);\n return;\n }\n setOpen(!isOpen);\n }}\n initial={reducedMotion ? false : { opacity: 0, scale: 0.9 }}\n animate={{ opacity: 1, scale: 1 }}\n exit={reducedMotion ? undefined : { opacity: 0, scale: 0.9 }}\n transition={{ duration: reducedMotion ? 0 : 0.15 }}\n className=\"h-full w-full rounded-full px-2 outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-inset\"\n >\n {compact}\n \n )}\n \n\n {isPanelOpen && (\n \n \n \n )}\n \n );\n}\n", "type": "registry:component" } ], "type": "registry:component" }