{ "name": "stack", "type": "registry:ui", "dependencies": [ "radix-ui" ], "files": [ { "path": "ui/stack.tsx", "type": "registry:ui", "content": "\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { Slot as SlotPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ninterface ItemDimension {\n itemId: number;\n size: number;\n}\n\ntype Side = \"top\" | \"bottom\";\n\nfunction getDataState(isExpanded: boolean) {\n return isExpanded ? \"expanded\" : \"collapsed\";\n}\n\ninterface StackContextValue {\n side: Side;\n childrenCount: number;\n itemCount: number;\n expandedItemCount: number;\n gap: number;\n scale: number;\n offset: number;\n expandOnHover: boolean;\n isExpanded: boolean;\n isInteracting: boolean;\n dimensions: ItemDimension[];\n setDimensions: React.Dispatch>;\n}\n\nconst StackContext = React.createContext(null);\n\nfunction useStackContext(consumerName: string) {\n const context = React.useContext(StackContext);\n if (!context) {\n throw new Error(`\\`${consumerName}\\` must be used within \\`Stack\\``);\n }\n return context;\n}\n\ninterface StackProps extends React.ComponentProps<\"div\"> {\n side?: Side;\n itemCount?: number;\n expandedItemCount?: number;\n gap?: number;\n scale?: number;\n offset?: number;\n expandOnHover?: boolean;\n asChild?: boolean;\n}\n\nfunction Stack(props: StackProps) {\n const {\n side = \"bottom\",\n itemCount = 3,\n expandedItemCount,\n gap = 8,\n scale = 0.05,\n offset = 10,\n className,\n children,\n style,\n onMouseEnter: onMouseEnterProp,\n onMouseLeave: onMouseLeaveProp,\n onMouseMove: onMouseMoveProp,\n onPointerDown: onPointerDownProp,\n onPointerUp: onPointerUpProp,\n expandOnHover = false,\n asChild,\n ...rootProps\n } = props;\n\n const [isExpanded, setIsExpanded] = React.useState(false);\n const [isInteracting, setIsInteracting] = React.useState(false);\n const [dimensions, setDimensions] = React.useState([]);\n\n const childrenArray = React.Children.toArray(children).filter(\n React.isValidElement,\n );\n const childrenCount = childrenArray.length;\n\n const effectiveExpandedItemCount = expandedItemCount ?? childrenCount;\n\n const onMouseEnter = React.useCallback(\n (event: React.MouseEvent) => {\n onMouseEnterProp?.(event);\n if (event.defaultPrevented) return;\n\n if (expandOnHover) {\n setIsExpanded(true);\n }\n },\n [expandOnHover, onMouseEnterProp],\n );\n\n const onMouseMove = React.useCallback(\n (event: React.MouseEvent) => {\n onMouseMoveProp?.(event);\n if (event.defaultPrevented) return;\n\n if (expandOnHover) {\n setIsExpanded(true);\n }\n },\n [expandOnHover, onMouseMoveProp],\n );\n\n const onMouseLeave = React.useCallback(\n (event: React.MouseEvent) => {\n onMouseLeaveProp?.(event);\n if (event.defaultPrevented) return;\n\n if (expandOnHover && !isInteracting) {\n setIsExpanded(false);\n }\n },\n [expandOnHover, isInteracting, onMouseLeaveProp],\n );\n\n const onPointerDown = React.useCallback(\n (event: React.PointerEvent) => {\n onPointerDownProp?.(event);\n if (event.defaultPrevented) return;\n\n setIsInteracting(true);\n },\n [onPointerDownProp],\n );\n\n const onPointerUp = React.useCallback(\n (event: React.PointerEvent) => {\n onPointerUpProp?.(event);\n if (event.defaultPrevented) return;\n\n setIsInteracting(false);\n },\n [onPointerUpProp],\n );\n\n const contextValue = React.useMemo(\n () => ({\n side,\n childrenCount,\n itemCount,\n expandedItemCount: effectiveExpandedItemCount,\n gap,\n scale,\n offset,\n expandOnHover,\n isExpanded,\n isInteracting,\n dimensions,\n setDimensions,\n }),\n [\n side,\n childrenCount,\n itemCount,\n effectiveExpandedItemCount,\n gap,\n scale,\n offset,\n expandOnHover,\n isExpanded,\n isInteracting,\n dimensions,\n ],\n );\n\n const RootPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n return (\n \n \n {childrenArray.map((child, index) => (\n \n {child}\n \n ))}\n \n \n );\n}\n\nconst stackItemWrapperVariants = cva(\n \"absolute w-full transition-all duration-300 ease-out\",\n {\n variants: {\n side: {\n top: [\n \"top-0 left-0 origin-top\",\n \"translate-y-[calc(var(--translate)*-1)] scale-[var(--item-scale)]\",\n \"after:absolute after:top-full after:left-0 after:w-full after:content-['']\",\n ],\n bottom: [\n \"bottom-0 left-0 origin-bottom\",\n \"translate-y-[var(--translate)] scale-[var(--item-scale)]\",\n \"after:absolute after:bottom-full after:left-0 after:w-full after:content-['']\",\n ],\n },\n isExpanded: {\n true: \"after:h-[calc(var(--gap)+1px)]\",\n false: \"\",\n },\n isVisible: {\n true: \"\",\n false: \"pointer-events-none\",\n },\n },\n },\n);\n\ntype StackItemWrapperElement = React.ComponentRef;\n\ninterface StackItemWrapperProps extends React.ComponentProps<\"div\"> {\n index: number;\n}\n\nfunction StackItemWrapper(props: StackItemWrapperProps) {\n const { children, className, index, style, ...itemProps } = props;\n\n const {\n side,\n childrenCount,\n itemCount,\n expandedItemCount,\n gap,\n scale,\n offset,\n isExpanded,\n dimensions,\n setDimensions,\n } = useStackContext(\"StackItemWrapper\");\n\n const itemRef = React.useRef(null);\n\n const isFront = index === 0;\n const isVisible = isExpanded ? index < expandedItemCount : index < itemCount;\n\n React.useEffect(() => {\n const itemNode = itemRef.current;\n if (itemNode) {\n const rect = itemNode.getBoundingClientRect();\n const measuredHeight = rect.height;\n const currentScale = 1 - index * scale;\n const naturalHeight = measuredHeight / currentScale;\n\n setDimensions((d) => {\n const existing = d.find((item) => item.itemId === index);\n if (!existing) {\n return [...d, { itemId: index, size: naturalHeight }];\n }\n return d;\n });\n }\n }, [index, scale, setDimensions]);\n\n const itemsSizeBefore = React.useMemo(() => {\n return dimensions.reduce((prev, curr) => {\n if (curr.itemId >= index) return prev;\n return prev + curr.size;\n }, 0);\n }, [dimensions, index]);\n\n const itemScale = isExpanded ? 1 : 1 - index * scale;\n const translateValue = isExpanded\n ? index * gap + itemsSizeBefore\n : index * offset;\n const zIndex = childrenCount - index;\n\n const opacity = !isVisible ? 0 : isExpanded ? 1 : 1 - index * 0.15;\n\n return (\n \n \n {children}\n \n \n );\n}\n\ninterface StackItemProps extends React.ComponentProps<\"div\"> {\n asChild?: boolean;\n}\n\nfunction StackItem(props: StackItemProps) {\n const { asChild, className, ...itemProps } = props;\n\n const ItemPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n return (\n \n );\n}\n\nexport { Stack, StackItem, type StackProps };\n", "target": "" } ] }