"use client"; import { AnimatePresence, type HTMLMotionProps, motion, useReducedMotion, type Variants, } from "motion/react"; import { Children, cloneElement, forwardRef, type HTMLAttributes, isValidElement, type MouseEvent, type ReactElement, type ReactNode, type Ref, useId, useState, } from "react"; import { SPRING_LAYOUT } from "@/lib/ease"; import { cn } from "@/lib/utils"; export interface SharedLayoutBgProps extends Omit, "children"> { children: ReactNode; /** Semantic container used for the children. */ as?: "div" | "ul"; /** Tailwind class applied to the moving pill. Defaults to a subtle foreground tint. */ pillClassName?: string; /** Horizontal inset of the pill relative to each row (px). Default 20. */ inset?: number; /** Optional positioning override for the pill wrapper inside each item. */ pillContainerClassName?: string; } const variants: Variants = { initial: { opacity: 0, filter: "blur(6px)" }, animate: { opacity: 1, filter: "blur(0px)" }, exit: (isActive: boolean) => !isActive ? { opacity: 0, filter: "blur(6px)" } : {}, }; const reducedVariants: Variants = { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: (isActive: boolean) => (!isActive ? { opacity: 0 } : {}), }; export const SharedLayoutBg = forwardRef( function SharedLayoutBg( { children, as = "div", className, onMouseLeave, pillClassName, pillContainerClassName, inset = 20, ...props }, forwardedRef, ) { const [activeId, setActiveId] = useState(null); const uid = useId(); const reduce = useReducedMotion(); const renderedChildren = Children.toArray(children) .filter(isValidElement) .map((child, index) => { const el = child as ReactElement<{ className?: string; onMouseEnter?: () => void; children?: ReactNode; }>; const childKey = el.key ? String(el.key) : `item-${index}`; return cloneElement( el, { key: childKey, className: cn("relative", el.props.className), onMouseEnter: () => { el.props.onMouseEnter?.(); setActiveId(childKey); }, }, <> {activeId !== null ? ( {activeId === childKey ? ( ) : null} ) : null}
{el.props.children}
, ); }); const handleMouseLeave = (event: MouseEvent) => { setActiveId(null); onMouseLeave?.(event); }; // layoutRoot scopes the pill's layout projection to this list, so fixed or // scrolled ancestors can't smear scroll offsets into its movement. return as === "ul" ? ( )} ref={forwardedRef as Ref} layoutRoot onMouseLeave={handleMouseLeave} className={cn("flex w-full flex-col", className)} > {renderedChildren} ) : ( )} ref={forwardedRef as Ref} layoutRoot onMouseLeave={handleMouseLeave} className={cn("flex w-full flex-col", className)} > {renderedChildren} ); }, );