{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "expandable-screen", "type": "registry:ui", "description": "A full-screen expandable component with morphing animations using shared layout IDs for smooth transitions", "dependencies": [ "motion", "lucide-react" ], "files": [ { "path": "registry/default/ui/expandable-screen.tsx", "content": "\"use client\"\n\nimport {\n createContext,\n useContext,\n useEffect,\n useState,\n type ReactNode,\n} from \"react\"\nimport { X } from \"lucide-react\"\nimport { AnimatePresence, motion } from \"motion/react\"\n\n// Context\ninterface ExpandableScreenContextValue {\n isExpanded: boolean\n expand: () => void\n collapse: () => void\n layoutId: string\n triggerRadius: string\n contentRadius: string\n animationDuration: number\n}\n\nconst ExpandableScreenContext =\n createContext(null)\n\nfunction useExpandableScreen() {\n const context = useContext(ExpandableScreenContext)\n if (!context) {\n throw new Error(\n \"useExpandableScreen must be used within an ExpandableScreen\"\n )\n }\n return context\n}\n\n// Root Component\ninterface ExpandableScreenProps {\n children: ReactNode\n defaultExpanded?: boolean\n onExpandChange?: (expanded: boolean) => void\n layoutId?: string\n triggerRadius?: string\n contentRadius?: string\n animationDuration?: number\n lockScroll?: boolean\n}\n\nexport function ExpandableScreen({\n children,\n defaultExpanded = false,\n onExpandChange,\n layoutId = \"expandable-card\",\n triggerRadius = \"100px\",\n contentRadius = \"24px\",\n animationDuration = 0.3,\n lockScroll = true,\n}: ExpandableScreenProps) {\n const [isExpanded, setIsExpanded] = useState(defaultExpanded)\n\n const expand = () => {\n setIsExpanded(true)\n onExpandChange?.(true)\n }\n\n const collapse = () => {\n setIsExpanded(false)\n onExpandChange?.(false)\n }\n\n useEffect(() => {\n if (lockScroll) {\n if (isExpanded) {\n document.body.style.overflow = \"hidden\"\n } else {\n document.body.style.overflow = \"unset\"\n }\n }\n }, [isExpanded, lockScroll])\n\n return (\n \n {children}\n \n )\n}\n\n// Trigger Component\ninterface ExpandableScreenTriggerProps {\n children: ReactNode\n className?: string\n}\n\nexport function ExpandableScreenTrigger({\n children,\n className = \"\",\n}: ExpandableScreenTriggerProps) {\n const { isExpanded, expand, layoutId, triggerRadius } = useExpandableScreen()\n\n return (\n \n {!isExpanded && (\n \n {/* Background layer with shared layoutId for morphing */}\n \n {/* Content layer that fades out on expand */}\n \n {children}\n \n \n )}\n \n )\n}\n\n// Content Component\ninterface ExpandableScreenContentProps {\n children: ReactNode\n className?: string\n showCloseButton?: boolean\n closeButtonClassName?: string\n}\n\nexport function ExpandableScreenContent({\n children,\n className = \"\",\n showCloseButton = true,\n closeButtonClassName = \"\",\n}: ExpandableScreenContentProps) {\n const { isExpanded, collapse, layoutId, contentRadius, animationDuration } =\n useExpandableScreen()\n\n return (\n \n {isExpanded && (\n
\n {/* Morphing background with shared layoutId */}\n \n \n {children}\n \n\n {showCloseButton && (\n \n \n \n )}\n \n
\n )}\n
\n )\n}\n\n// Background Component (optional)\ninterface ExpandableScreenBackgroundProps {\n trigger?: ReactNode\n content?: ReactNode\n className?: string\n}\n\nexport function ExpandableScreenBackground({\n trigger,\n content,\n className = \"\",\n}: ExpandableScreenBackgroundProps) {\n const { isExpanded } = useExpandableScreen()\n\n if (isExpanded && content) {\n return
{content}
\n }\n\n if (!isExpanded && trigger) {\n return
{trigger}
\n }\n\n return null\n}\n\nexport { useExpandableScreen }\n", "type": "registry:ui" } ] }