{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"ui-motion-highlight","type":"registry:component","title":"Motion Highlight","description":"Animated highlight primitive for active item emphasis.","version":"1.0.0","status":"ga","files":[{"path":"src/components/ui/motion-highlight.tsx","type":"registry:component","content":"'use client'\n/* eslint-disable react-hooks/preserve-manual-memoization, react-hooks/refs */\n\nimport * as React from 'react'\n\nimport type { Transition } from 'motion/react'\nimport { AnimatePresence, 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\n  if (!context) {\n    throw new Error('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> = BaseMotionHighlightProps<T> &\n  ParentModeMotionHighlightProps & {\n    mode: 'parent'\n    controlledItems: true\n    children: React.ReactNode\n  }\n\ntype ControlledChildrenModeMotionHighlightProps<T extends string> = BaseMotionHighlightProps<T> & {\n  mode?: 'children' | undefined\n  controlledItems: true\n  children: React.ReactNode\n}\n\ntype UncontrolledParentModeMotionHighlightProps<T extends string> = 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> = 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>({ ref, ...props }: 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\n  React.useImperativeHandle(ref, () => localRef.current as HTMLDivElement)\n\n  const [activeValue, setActiveValue] = React.useState<T | null>(value ?? defaultValue ?? null)\n  const [boundsState, setBoundsState] = React.useState<Bounds | null>(null)\n  const [activeClassNameState, setActiveClassNameState] = 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)?.boundsOffset ?? {\n        top: 0,\n        left: 0,\n        width: 0,\n        height: 0\n      }\n\n      const containerRect = localRef.current.getBoundingClientRect()\n\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\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\n    if (!container) return\n\n    const onScroll = () => {\n      if (!activeValue) return\n      const activeEl = container.querySelector<HTMLElement>(`[data-value=\"${activeValue}\"][data-highlight=\"true\"]`)\n\n      if (activeEl) safeSetBounds(activeEl.getBoundingClientRect())\n    }\n\n    container.addEventListener('scroll', onScroll, { passive: true })\n\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('relative', (props as ParentModeMotionHighlightProps)?.containerClassName)}\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('bg-muted absolute z-0', className, activeClassNameState)}\n                />\n              )}\n            </AnimatePresence>\n            {children}\n          </div>\n        )\n      }\n\n      return children\n    },\n    [mode, props, boundsState, transition, exitDelay, className, activeClassNameState]\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)?.forceUpdateBounds\n      }}\n    >\n      {enabled\n        ? controlledItems\n          ? render(children)\n          : render(\n              React.Children.map(children, (child, index) => (\n                <MotionHighlightItem key={index} className={props?.itemsClassName}>\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>>((acc, key) => {\n    if ((element.props as Record<string, unknown>)[key] === undefined) {\n      acc[key] = dataAttributes[key]\n    }\n\n    return acc\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\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 = 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\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 = forceUpdateBounds === true || (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\n          return\n        }\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('bg-muted absolute inset-0 z-0', contextClassName, activeClassName)}\n                transition={itemTransition}\n                initial={{ opacity: 0 }}\n                animate={{ opacity: 1 }}\n                exit={{\n                  opacity: 0,\n                  transition: {\n                    ...itemTransition,\n                    delay: (itemTransition?.delay ?? 0) + (exitDelay ?? contextExitDelay ?? 0)\n                  }\n                }}\n                {...dataAttributes}\n              />\n            )}\n          </AnimatePresence>\n\n          <div data-slot='motion-highlight-item' className={cn('relative z-[1]', className)} {...dataAttributes}>\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('bg-muted absolute inset-0 z-0', contextClassName, activeClassName)}\n              transition={itemTransition}\n              initial={{ opacity: 0 }}\n              animate={{ opacity: 1 }}\n              exit={{\n                opacity: 0,\n                transition: {\n                  ...itemTransition,\n                  delay: (itemTransition?.delay ?? 0) + (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"}]}