{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"motion-dock","type":"registry:block","title":"Motion Dock","description":"A reusable component where tooltips slide smoothly between dock items, making the motion dock feel natural and easy to use.","dependencies":["motion"],"files":[{"path":"registry/100xui/blocks/motion-dock/components/motion-dock.tsx","content":"\"use client\";\n\nimport React from \"react\";\nimport {\n  useAnimate,\n  motion,\n  AnimatePresence,\n  usePresence,\n  useMotionValue,\n  type Transition,\n  type AnimationPlaybackControlsWithThen,\n  type HTMLMotionProps,\n} from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\nimport { usePrevious } from \"@/hooks/use-previous\";\nimport { useDebouncedState } from \"@/hooks/use-debounced-state\";\n\ninterface DockItem extends Omit<HTMLMotionProps<\"button\">, \"ref\"> {\n  icon: React.ReactElement;\n  tooltip: string;\n}\n\nexport interface MotionDockProps extends React.ComponentProps<\"div\"> {\n  dockItems: DockItem[];\n  tooltipBorderRadius?: React.CSSProperties[\"borderRadius\"];\n}\nconst TRANSITION: Transition = {\n  duration: 0.2,\n  ease: [0.76, 0, 0.24, 1], // easeInOutQuart\n};\nexport function MotionDock({\n  dockItems,\n  className,\n  tooltipBorderRadius = \"var(--radius-sm)\",\n  ...rest\n}: MotionDockProps) {\n  const [activeItem, setActiveItem] = useDebouncedState<number>(-1, 100);\n  const dockRef = React.useRef<HTMLDivElement>(null);\n\n  const handleReset = () => setActiveItem(-1);\n\n  return (\n    <div className={cn(className, \"!relative !w-fit\")} {...rest}>\n      <div\n        ref={dockRef}\n        className=\"bg-background text-foreground border-border flex gap-0.5 rounded-full border p-1 shadow-sm\"\n        onMouseLeave={handleReset}\n        onBlurCapture={handleReset}\n      >\n        {dockItems.map(\n          ({ icon, tooltip, onMouseEnter, onFocus, className, ...rest }, i) => (\n            <motion.button\n              key={tooltip}\n              aria-label={tooltip}\n              onFocus={(e) => {\n                setActiveItem(i);\n                onFocus?.(e);\n              }}\n              onMouseEnter={(e) => {\n                setActiveItem(i);\n                onMouseEnter?.(e);\n              }}\n              data-dockitem={i}\n              className={cn(\n                \"hover:text-accent-foreground hover:bg-accent focus-visible:text-accent-foreground focus-visible:bg-accent focus-visible:ring-ring focus-visible:ring-offset-background cursor-pointer rounded-full p-1.5 transition-colors duration-150 ease-out focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-none\",\n                className\n              )}\n              {...rest}\n            >\n              {icon}\n            </motion.button>\n          )\n        )}\n      </div>\n      <AnimatePresence>\n        {activeItem >= 0 && (\n          <ToolTipsContainer\n            activeItem={activeItem}\n            dockRef={dockRef as React.RefObject<HTMLDivElement>}\n            tooltips={dockItems.map(({ tooltip }) => tooltip)}\n            tooltipBorderRadius={tooltipBorderRadius}\n          />\n        )}\n      </AnimatePresence>\n    </div>\n  );\n}\n\ninterface TooltipsContainerProps {\n  dockRef: React.RefObject<HTMLDivElement>;\n  activeItem: number;\n  tooltips: string[];\n  tooltipBorderRadius: React.CSSProperties[\"borderRadius\"];\n}\nfunction ToolTipsContainer({\n  dockRef,\n  activeItem,\n  tooltips,\n  tooltipBorderRadius,\n}: TooltipsContainerProps) {\n  const prevMouseIn = usePrevious<number>(activeItem);\n  const [isPresent, safeToRemove] = usePresence();\n  const [tooltipContainerScope, animate] = useAnimate();\n  const x = useMotionValue(0);\n\n  const getTranslateX = React.useCallback(\n    (activeItem: number) => {\n      const hoverDockItem = dockRef.current.querySelector(\n        `[data-dockitem=\"${activeItem}\"]`\n      );\n      const { left: hoverDockItemLeft, width: hoverDockItemWidth } =\n        hoverDockItem!.getBoundingClientRect();\n      const finalPosition = hoverDockItemLeft + hoverDockItemWidth / 2;\n\n      const correspondingTooltip = tooltipContainerScope.current.querySelector(\n        `[data-tooltip=\"${activeItem}\"]`\n      );\n      const { left: tooltipLeft, width: tooltipWidth } =\n        correspondingTooltip!.getBoundingClientRect();\n      const currentPosition = tooltipLeft + tooltipWidth / 2;\n\n      const relativeTranslateX = finalPosition - currentPosition;\n      const translateX = relativeTranslateX + x.get();\n      return translateX;\n    },\n    [x, dockRef, tooltipContainerScope]\n  );\n\n  const getClipPath = React.useCallback(\n    (activeItem: number) => {\n      let left = 0;\n      let right = 0;\n      for (let j = 0; j < tooltips.length; j++) {\n        const { width } = tooltipContainerScope.current\n          .querySelector(`[data-tooltip=\"${j}\"]`)\n          .getBoundingClientRect();\n        if (j < activeItem) {\n          left += width;\n        } else if (j > activeItem) {\n          right += width;\n        }\n      }\n      const clipPath = `inset(0px ${right}px 0px ${left}px round ${tooltipBorderRadius}`;\n\n      return clipPath;\n    },\n    [tooltips.length, tooltipContainerScope, tooltipBorderRadius]\n  );\n\n  React.useEffect(() => {\n    let control: AnimationPlaybackControlsWithThen | undefined = undefined;\n    if (isPresent) {\n      const keyframes = {\n        clipPath: getClipPath(activeItem),\n        x: getTranslateX(activeItem),\n      };\n      if (prevMouseIn === undefined) {\n        const enterAnimation = async () => {\n          if (typeof control !== \"undefined\") {\n            control.stop();\n          }\n          await animate(tooltipContainerScope.current, keyframes, {\n            duration: 0,\n          });\n          await animate(\n            tooltipContainerScope.current,\n            { opacity: 1 },\n            TRANSITION\n          );\n        };\n        enterAnimation();\n      } else {\n        const intermediateAnimation = () => {\n          animate(tooltipContainerScope.current, keyframes, TRANSITION);\n        };\n        intermediateAnimation();\n      }\n    } else {\n      const exitAnimation = async () => {\n        control = await animate(\n          tooltipContainerScope.current,\n          { opacity: 0 },\n          TRANSITION\n        );\n        safeToRemove();\n      };\n      exitAnimation();\n    }\n  }, [\n    animate,\n    safeToRemove,\n    isPresent,\n    prevMouseIn,\n    activeItem,\n    getClipPath,\n    getTranslateX,\n    tooltipContainerScope,\n  ]);\n\n  return (\n    <motion.div\n      ref={tooltipContainerScope}\n      initial={{\n        opacity: 0,\n      }}\n      style={{\n        x,\n      }}\n      className=\"bg-primary text-primary-foreground absolute bottom-[calc(100%_+_var(--spacing)_*_1)] flex flex-nowrap py-1 text-xs\"\n    >\n      {tooltips.map((tooltip, i) => (\n        <div\n          key={tooltip}\n          className=\"px-2 text-nowrap whitespace-nowrap\"\n          data-tooltip={i}\n        >\n          {tooltip}\n        </div>\n      ))}\n    </motion.div>\n  );\n}\n","type":"registry:component"},{"path":"registry/100xui/shared/hooks/use-debounced-state.ts","content":"import React from \"react\";\n\nexport function useDebouncedState<T>(initialValue: T, delay: number) {\n  const [state, setState] = React.useState(initialValue);\n  const timeoutRef = React.useRef<NodeJS.Timeout>(undefined);\n\n  const setDebouncedState = React.useCallback(\n    (newState: T) => {\n      clearTimeout(timeoutRef.current);\n      timeoutRef.current = setTimeout(() => setState(newState), delay);\n    },\n    [delay],\n  );\n\n  React.useEffect(() => {\n    return () => clearTimeout(timeoutRef.current);\n  }, []);\n\n  return [state, setDebouncedState] as const;\n}\n","type":"registry:hook"},{"path":"registry/100xui/shared/hooks/use-previous.ts","content":"import React from \"react\";\n\nexport function usePrevious<T>(state: T): T | undefined {\n  const prevState = React.useRef<T | undefined>(undefined);\n  React.useEffect(() => {\n    prevState.current = state;\n  }, [state]);\n  return prevState.current;\n}\n","type":"registry:hook"}]}