Expanding Screen Card
Expanding Screen Card é um componente para React e Tailwind CSS, da biblioteca Cult UI, com licença MIT. Copia e cola no teu projeto.
Cult UI
MIT
other
O que é
Expanding Screen Card é um componente para React e Tailwind CSS, da biblioteca Cult UI, com licença MIT. Copia e cola no teu projeto.
Como usar
Abre o componente no catálogo, carrega em copiar, e cola no teu projeto. O prompt copiado leva o código e as regras para o teu agente o reproduzir sem alterar nada.
Cinco cópias grátis por mês. Sem cartão.
Precisa de
npm i lucide-react motion
Licença
Este componente vem de Cult UI e é redistribuído sob a licença MIT, que permite uso comercial. O aviso de copyright viaja com o código.
Expanding Screen Card
"use client"
import {
createContext,
useContext,
useEffect,
useState,
type ReactNode,
} from "react"
import { X } from "lucide-react"
import { AnimatePresence, motion } from "motion/react"
// Context
interface ExpandableScreenContextValue {
isExpanded: boolean
expand: () => void
collapse: () => void
layoutId: string
triggerRadius: string
contentRadius: string
animationDuration: number
}
const ExpandableScreenContext =
createContext<ExpandableScreenContextValue | null>(null)
function useExpandableScreen() {
const context = useContext(ExpandableScreenContext)
if (!context) {
throw new Error(
"useExpandableScreen must be used within an ExpandableScreen"
)
}
return context
}
// Root Component
interface ExpandableScreenProps {
children: ReactNode
defaultExpanded?: boolean
onExpandChange?: (expanded: boolean) => void
layoutId?: string
triggerRadius?: string
contentRadius?: string
animationDuration?: number
lockScroll?: boolean
}
export function ExpandableScreen({
children,
defaultExpanded = false,
onExpandChange,
layoutId = "expandable-card",
triggerRadius = "100px",
contentRadius = "24px",
animationDuration = 0.3,
lockScroll = true,
}: ExpandableScreenProps) {
const [isExpanded, setIsExpanded] = useState(defaultExpanded)
const expand = () => {
setIsExpanded(true)
onExpandChange?.(true)
}
const collapse = () => {
setIsExpanded(false)
onExpandChange?.(false)
}
useEffect(() => {
if (lockScroll) {
if (isExpanded) {
document.body.style.overflow = "hidden"
} else {
document.body.style.overflow = "unset"
}
}
}, [isExpanded, lockScroll])
return (
<ExpandableScreenContext.Provider
value={{
isExpanded,
expand,
collapse,
layoutId,
triggerRadius,
contentRadius,
animationDuration,
}}
>
{children}
</ExpandableScreenContext.Provider>
)
}
// Trigger Component
interface ExpandableScreenTriggerProps {
children: ReactNode
className?: string
}
export function ExpandableScreenTrigger({
children,
className = "",
}: ExpandableScreenTriggerProps) {
const { isExpanded, expand, layoutId, triggerRadius } = useExpandableScreen()
return (
<AnimatePresence initial={false}>
{!isExpanded && (
<motion.div className={`inline-block relative ${className}`}>
{/* Background layer with shared layoutId for morphing */}
<motion.div
style={{
borderRadius: triggerRadius,
}}
layout
layoutId={layoutId}
className="absolute inset-0 transform-gpu will-change-transform"
/>
{/* Content layer that fades out on expand */}
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.2 }}
exit={{ opacity: 0, scale: 0.8 }}
layout={false}
onClick={expand}
className="relative cursor-pointer"
>
{children}
</motion.div>
</motion.div>
)}
</AnimatePresence>
)
}
// Content Component
interface ExpandableScreenContentProps {
children: ReactNode
className?: string
showCloseButton?: boolean
closeButtonClassName?: string
}
export function ExpandableScreenContent({
children,
className = "",
showCloseButton = true,
closeButtonClassName = "",
}: ExpandableScreenContentProps) {
const { isExpanded, collapse, layoutId, contentRadius, animationDuration } =
useExpandableScreen()
return (
<AnimatePresence initial={false}>
{isExpanded && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-3 sm:p-2">
{/* Morphing background with shared layoutId */}
<motion.div
layoutId={layoutId}
transition={{ duration: animationDuration }}
style={{
borderRadius: contentRadius,
}}
layout
className={`relative flex h-full w-full overflow-y-auto transform-gpu will-change-transform ${className}`}
>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.15, duration: 0.4 }}
className="relative z-20 w-full"
>
{children}
</motion.div>
{showCloseButton && (
<motion.button
onClick={collapse}
className={`absolute right-6 top-6 z-30 flex h-10 w-10 items-center justify-center transition-colors rounded-full ${
closeButtonClassName ||
"text-primary-foreground bg-transparent hover:bg-primary-foreground/10"
}`}
aria-label="Close"
>
<X className="h-5 w-5" />
</motion.button>
)}
</motion.div>
</div>
)}
</AnimatePresence>
)
}
// Background Component (optional)
interface ExpandableScreenBackgroundProps {
trigger?: ReactNode
content?: ReactNode
className?: string
}
export function ExpandableScreenBackground({
trigger,
content,
className = "",
}: ExpandableScreenBackgroundProps) {
const { isExpanded } = useExpandableScreen()
if (isExpanded && content) {
return <div className={className}>{content}</div>
}
if (!isExpanded && trigger) {
return <div className={className}>{trigger}</div>
}
return null
}
export { useExpandableScreen }