Fortura/apps/www/public/r/motion-highlight.json
2025-08-20 04:12:49 -06:00

18 lines
17 KiB
JSON

{
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
"name": "motion-highlight",
"type": "registry:ui",
"title": "Motion Highlight",
"description": "Motion highlight component that displays the motion highlight effect.",
"dependencies": [
"motion"
],
"files": [
{
"path": "registry/effects/motion-highlight/index.tsx",
"content": "'use client';\n\nimport * as React from 'react';\nimport { AnimatePresence, Transition, motion } from 'motion/react';\n\nimport { cn } from '@/lib/utils';\n\ntype MotionHighlightMode = 'children' | 'parent';\n\ntype Bounds = {\n top: number;\n left: number;\n width: number;\n height: number;\n};\n\ntype MotionHighlightContextType<T extends string> = {\n mode: MotionHighlightMode;\n activeValue: T | null;\n setActiveValue: (value: T | null) => void;\n setBounds: (bounds: DOMRect) => void;\n clearBounds: () => void;\n id: string;\n hover: boolean;\n className?: string;\n activeClassName?: string;\n setActiveClassName: (className: string) => void;\n transition?: Transition;\n disabled?: boolean;\n enabled?: boolean;\n exitDelay?: number;\n forceUpdateBounds?: boolean;\n};\n\nconst MotionHighlightContext = React.createContext<\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n MotionHighlightContextType<any> | undefined\n>(undefined);\n\nfunction useMotionHighlight<T extends string>(): MotionHighlightContextType<T> {\n const context = React.useContext(MotionHighlightContext);\n if (!context) {\n throw new Error(\n 'useMotionHighlight must be used within a MotionHighlightProvider',\n );\n }\n return context as unknown as MotionHighlightContextType<T>;\n}\n\ntype BaseMotionHighlightProps<T extends string> = {\n mode?: MotionHighlightMode;\n value?: T | null;\n defaultValue?: T | null;\n onValueChange?: (value: T | null) => void;\n className?: string;\n transition?: Transition;\n hover?: boolean;\n disabled?: boolean;\n enabled?: boolean;\n exitDelay?: number;\n};\n\ntype ParentModeMotionHighlightProps = {\n boundsOffset?: Partial<Bounds>;\n containerClassName?: string;\n forceUpdateBounds?: boolean;\n};\n\ntype ControlledParentModeMotionHighlightProps<T extends string> =\n BaseMotionHighlightProps<T> &\n ParentModeMotionHighlightProps & {\n mode: 'parent';\n controlledItems: true;\n children: React.ReactNode;\n };\n\ntype ControlledChildrenModeMotionHighlightProps<T extends string> =\n BaseMotionHighlightProps<T> & {\n mode?: 'children' | undefined;\n controlledItems: true;\n children: React.ReactNode;\n };\n\ntype UncontrolledParentModeMotionHighlightProps<T extends string> =\n BaseMotionHighlightProps<T> &\n ParentModeMotionHighlightProps & {\n mode: 'parent';\n controlledItems?: false;\n itemsClassName?: string;\n children: React.ReactElement | React.ReactElement[];\n };\n\ntype UncontrolledChildrenModeMotionHighlightProps<T extends string> =\n BaseMotionHighlightProps<T> & {\n mode?: 'children';\n controlledItems?: false;\n itemsClassName?: string;\n children: React.ReactElement | React.ReactElement[];\n };\n\ntype MotionHighlightProps<T extends string> = React.ComponentProps<'div'> &\n (\n | ControlledParentModeMotionHighlightProps<T>\n | ControlledChildrenModeMotionHighlightProps<T>\n | UncontrolledParentModeMotionHighlightProps<T>\n | UncontrolledChildrenModeMotionHighlightProps<T>\n );\n\nfunction MotionHighlight<T extends string>({\n ref,\n ...props\n}: MotionHighlightProps<T>) {\n const {\n children,\n value,\n defaultValue,\n onValueChange,\n className,\n transition = { type: 'spring', stiffness: 350, damping: 35 },\n hover = false,\n enabled = true,\n controlledItems,\n disabled = false,\n exitDelay = 0.2,\n mode = 'children',\n } = props;\n\n const localRef = React.useRef<HTMLDivElement>(null);\n React.useImperativeHandle(ref, () => localRef.current as HTMLDivElement);\n\n const [activeValue, setActiveValue] = React.useState<T | null>(\n value ?? defaultValue ?? null,\n );\n const [boundsState, setBoundsState] = React.useState<Bounds | null>(null);\n const [activeClassNameState, setActiveClassNameState] =\n React.useState<string>('');\n\n const safeSetActiveValue = React.useCallback(\n (id: T | null) => {\n setActiveValue((prev) => (prev === id ? prev : id));\n if (id !== activeValue) onValueChange?.(id as T);\n },\n [activeValue, onValueChange],\n );\n\n const safeSetBounds = React.useCallback(\n (bounds: DOMRect) => {\n if (!localRef.current) return;\n\n const boundsOffset = (props as ParentModeMotionHighlightProps)\n ?.boundsOffset ?? {\n top: 0,\n left: 0,\n width: 0,\n height: 0,\n };\n\n const containerRect = localRef.current.getBoundingClientRect();\n const newBounds: Bounds = {\n top: bounds.top - containerRect.top + (boundsOffset.top ?? 0),\n left: bounds.left - containerRect.left + (boundsOffset.left ?? 0),\n width: bounds.width + (boundsOffset.width ?? 0),\n height: bounds.height + (boundsOffset.height ?? 0),\n };\n\n setBoundsState((prev) => {\n if (\n prev &&\n prev.top === newBounds.top &&\n prev.left === newBounds.left &&\n prev.width === newBounds.width &&\n prev.height === newBounds.height\n ) {\n return prev;\n }\n return newBounds;\n });\n },\n [props],\n );\n\n const clearBounds = React.useCallback(() => {\n setBoundsState((prev) => (prev === null ? prev : null));\n }, []);\n\n React.useEffect(() => {\n if (value !== undefined) setActiveValue(value);\n else if (defaultValue !== undefined) setActiveValue(defaultValue);\n }, [value, defaultValue]);\n\n const id = React.useId();\n\n React.useEffect(() => {\n if (mode !== 'parent') return;\n const container = localRef.current;\n if (!container) return;\n\n const onScroll = () => {\n if (!activeValue) return;\n const activeEl = container.querySelector<HTMLElement>(\n `[data-value=\"${activeValue}\"][data-highlight=\"true\"]`,\n );\n if (activeEl) safeSetBounds(activeEl.getBoundingClientRect());\n };\n\n container.addEventListener('scroll', onScroll, { passive: true });\n return () => container.removeEventListener('scroll', onScroll);\n }, [mode, activeValue, safeSetBounds]);\n\n const render = React.useCallback(\n (children: React.ReactNode) => {\n if (mode === 'parent') {\n return (\n <div\n ref={localRef}\n data-slot=\"motion-highlight-container\"\n className={cn(\n 'relative',\n (props as ParentModeMotionHighlightProps)?.containerClassName,\n )}\n >\n <AnimatePresence initial={false}>\n {boundsState && (\n <motion.div\n data-slot=\"motion-highlight\"\n animate={{\n top: boundsState.top,\n left: boundsState.left,\n width: boundsState.width,\n height: boundsState.height,\n opacity: 1,\n }}\n initial={{\n top: boundsState.top,\n left: boundsState.left,\n width: boundsState.width,\n height: boundsState.height,\n opacity: 0,\n }}\n exit={{\n opacity: 0,\n transition: {\n ...transition,\n delay: (transition?.delay ?? 0) + (exitDelay ?? 0),\n },\n }}\n transition={transition}\n className={cn(\n 'absolute bg-muted z-0',\n className,\n activeClassNameState,\n )}\n />\n )}\n </AnimatePresence>\n {children}\n </div>\n );\n }\n\n return children;\n },\n [\n mode,\n props,\n boundsState,\n transition,\n exitDelay,\n className,\n activeClassNameState,\n ],\n );\n\n return (\n <MotionHighlightContext.Provider\n value={{\n mode,\n activeValue,\n setActiveValue: safeSetActiveValue,\n id,\n hover,\n className,\n transition,\n disabled,\n enabled,\n exitDelay,\n setBounds: safeSetBounds,\n clearBounds,\n activeClassName: activeClassNameState,\n setActiveClassName: setActiveClassNameState,\n forceUpdateBounds: (props as ParentModeMotionHighlightProps)\n ?.forceUpdateBounds,\n }}\n >\n {enabled\n ? controlledItems\n ? render(children)\n : render(\n React.Children.map(children, (child, index) => (\n <MotionHighlightItem\n key={index}\n className={props?.itemsClassName}\n >\n {child}\n </MotionHighlightItem>\n )),\n )\n : children}\n </MotionHighlightContext.Provider>\n );\n}\n\nfunction getNonOverridingDataAttributes(\n element: React.ReactElement,\n dataAttributes: Record<string, unknown>,\n): Record<string, unknown> {\n return Object.keys(dataAttributes).reduce<Record<string, unknown>>(\n (acc, key) => {\n if ((element.props as Record<string, unknown>)[key] === undefined) {\n acc[key] = dataAttributes[key];\n }\n return acc;\n },\n {},\n );\n}\n\ntype ExtendedChildProps = React.ComponentProps<'div'> & {\n id?: string;\n ref?: React.Ref<HTMLElement>;\n 'data-active'?: string;\n 'data-value'?: string;\n 'data-disabled'?: boolean;\n 'data-highlight'?: boolean;\n 'data-slot'?: string;\n};\n\ntype MotionHighlightItemProps = React.ComponentProps<'div'> & {\n children: React.ReactElement;\n id?: string;\n value?: string;\n className?: string;\n transition?: Transition;\n activeClassName?: string;\n disabled?: boolean;\n exitDelay?: number;\n asChild?: boolean;\n forceUpdateBounds?: boolean;\n};\n\nfunction MotionHighlightItem({\n ref,\n children,\n id,\n value,\n className,\n transition,\n disabled = false,\n activeClassName,\n exitDelay,\n asChild = false,\n forceUpdateBounds,\n ...props\n}: MotionHighlightItemProps) {\n const itemId = React.useId();\n const {\n activeValue,\n setActiveValue,\n mode,\n setBounds,\n clearBounds,\n hover,\n enabled,\n className: contextClassName,\n transition: contextTransition,\n id: contextId,\n disabled: contextDisabled,\n exitDelay: contextExitDelay,\n forceUpdateBounds: contextForceUpdateBounds,\n setActiveClassName,\n } = useMotionHighlight();\n\n const element = children as React.ReactElement<ExtendedChildProps>;\n const childValue =\n id ?? value ?? element.props?.['data-value'] ?? element.props?.id ?? itemId;\n const isActive = activeValue === childValue;\n const isDisabled = disabled === undefined ? contextDisabled : disabled;\n const itemTransition = transition ?? contextTransition;\n\n const localRef = React.useRef<HTMLDivElement>(null);\n React.useImperativeHandle(ref, () => localRef.current as HTMLDivElement);\n\n React.useEffect(() => {\n if (mode !== 'parent') return;\n let rafId: number;\n let previousBounds: Bounds | null = null;\n const shouldUpdateBounds =\n forceUpdateBounds === true ||\n (contextForceUpdateBounds && forceUpdateBounds !== false);\n\n const updateBounds = () => {\n if (!localRef.current) return;\n\n const bounds = localRef.current.getBoundingClientRect();\n\n if (shouldUpdateBounds) {\n if (\n previousBounds &&\n previousBounds.top === bounds.top &&\n previousBounds.left === bounds.left &&\n previousBounds.width === bounds.width &&\n previousBounds.height === bounds.height\n ) {\n rafId = requestAnimationFrame(updateBounds);\n return;\n }\n previousBounds = bounds;\n rafId = requestAnimationFrame(updateBounds);\n }\n\n setBounds(bounds);\n };\n\n if (isActive) {\n updateBounds();\n setActiveClassName(activeClassName ?? '');\n } else if (!activeValue) clearBounds();\n\n if (shouldUpdateBounds) return () => cancelAnimationFrame(rafId);\n }, [\n mode,\n isActive,\n activeValue,\n setBounds,\n clearBounds,\n activeClassName,\n setActiveClassName,\n forceUpdateBounds,\n contextForceUpdateBounds,\n ]);\n\n if (!React.isValidElement(children)) return children;\n\n const dataAttributes = {\n 'data-active': isActive ? 'true' : 'false',\n 'aria-selected': isActive,\n 'data-disabled': isDisabled,\n 'data-value': childValue,\n 'data-highlight': true,\n };\n\n const commonHandlers = hover\n ? {\n onMouseEnter: (e: React.MouseEvent<HTMLDivElement>) => {\n setActiveValue(childValue);\n element.props.onMouseEnter?.(e);\n },\n onMouseLeave: (e: React.MouseEvent<HTMLDivElement>) => {\n setActiveValue(null);\n element.props.onMouseLeave?.(e);\n },\n }\n : {\n onClick: (e: React.MouseEvent<HTMLDivElement>) => {\n setActiveValue(childValue);\n element.props.onClick?.(e);\n },\n };\n\n if (asChild) {\n if (mode === 'children') {\n return React.cloneElement(\n element,\n {\n key: childValue,\n ref: localRef,\n className: cn('relative', element.props.className),\n ...getNonOverridingDataAttributes(element, {\n ...dataAttributes,\n 'data-slot': 'motion-highlight-item-container',\n }),\n ...commonHandlers,\n ...props,\n },\n <>\n <AnimatePresence initial={false}>\n {isActive && !isDisabled && (\n <motion.div\n layoutId={`transition-background-${contextId}`}\n data-slot=\"motion-highlight\"\n className={cn(\n 'absolute inset-0 bg-muted z-0',\n contextClassName,\n activeClassName,\n )}\n transition={itemTransition}\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{\n opacity: 0,\n transition: {\n ...itemTransition,\n delay:\n (itemTransition?.delay ?? 0) +\n (exitDelay ?? contextExitDelay ?? 0),\n },\n }}\n {...dataAttributes}\n />\n )}\n </AnimatePresence>\n\n <div\n data-slot=\"motion-highlight-item\"\n className={cn('relative z-[1]', className)}\n {...dataAttributes}\n >\n {children}\n </div>\n </>,\n );\n }\n\n return React.cloneElement(element, {\n ref: localRef,\n ...getNonOverridingDataAttributes(element, {\n ...dataAttributes,\n 'data-slot': 'motion-highlight-item',\n }),\n ...commonHandlers,\n });\n }\n\n return enabled ? (\n <div\n key={childValue}\n ref={localRef}\n data-slot=\"motion-highlight-item-container\"\n className={cn(mode === 'children' && 'relative', className)}\n {...dataAttributes}\n {...props}\n {...commonHandlers}\n >\n {mode === 'children' && (\n <AnimatePresence initial={false}>\n {isActive && !isDisabled && (\n <motion.div\n layoutId={`transition-background-${contextId}`}\n data-slot=\"motion-highlight\"\n className={cn(\n 'absolute inset-0 bg-muted z-0',\n contextClassName,\n activeClassName,\n )}\n transition={itemTransition}\n initial={{ opacity: 0 }}\n animate={{ opacity: 1 }}\n exit={{\n opacity: 0,\n transition: {\n ...itemTransition,\n delay:\n (itemTransition?.delay ?? 0) +\n (exitDelay ?? contextExitDelay ?? 0),\n },\n }}\n {...dataAttributes}\n />\n )}\n </AnimatePresence>\n )}\n\n {React.cloneElement(element, {\n className: cn('relative z-[1]', element.props.className),\n ...getNonOverridingDataAttributes(element, {\n ...dataAttributes,\n 'data-slot': 'motion-highlight-item',\n }),\n })}\n </div>\n ) : (\n children\n );\n}\n\nexport {\n MotionHighlight,\n MotionHighlightItem,\n useMotionHighlight,\n type MotionHighlightProps,\n type MotionHighlightItemProps,\n};\n",
"type": "registry:ui",
"target": "components/animate-ui/effects/motion-highlight.tsx"
}
]
}