{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A CursorFollow component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport {\n  motion,\n  useMotionValue,\n  useReducedMotion,\n  useSpring,\n} from \"motion/react\";\nimport type React from \"react\";\nimport { useEffect, useRef, useState } from \"react\";\n\nimport { useCursorPosition } from \"./use-cursor-position\";\n\nexport interface CursorFollowProps {\n  children: React.ReactNode;\n  className?: string;\n}\n\nconst CIRCLE_SIZE = 16;\nconst MIN_BUBBLE_WIDTH = 40;\nconst BUBBLE_HEIGHT = 40;\nconst TEXT_PADDING = 32;\n\nconst CursorFollow: React.FC<CursorFollowProps> = ({\n  children,\n  className = \"\",\n}) => {\n  const { x: mouseX, y: mouseY } = useCursorPosition();\n  const [cursorText, setCursorText] = useState<string | null>(null);\n  const [pendingText, setPendingText] = useState<string | null>(null);\n  const [textWidth, setTextWidth] = useState<number>(0);\n  const measureRef = useRef<HTMLSpanElement>(null);\n  const shouldReduceMotion = useReducedMotion();\n\n  // Motion values for smooth follow\n  const x = useMotionValue(0);\n  const y = useMotionValue(0);\n  const springX = useSpring(x, { damping: 40, stiffness: 350 });\n  const springY = useSpring(y, { damping: 40, stiffness: 350 });\n\n  // Calculate bubble width and height\n  const bubbleWidth = cursorText\n    ? Math.max(textWidth + TEXT_PADDING, MIN_BUBBLE_WIDTH)\n    : CIRCLE_SIZE;\n  const bubbleHeight = cursorText ? BUBBLE_HEIGHT : CIRCLE_SIZE;\n\n  // Update target position on mouse move\n  useEffect(() => {\n    x.set(mouseX - bubbleWidth / 2);\n    y.set(mouseY - bubbleHeight / 2);\n  }, [mouseX, mouseY, bubbleWidth, bubbleHeight, x, y]);\n\n  // Pre-measure text width before showing bubble\n  useEffect(() => {\n    if (pendingText && measureRef.current) {\n      const width = measureRef.current.offsetWidth;\n      setTextWidth(width);\n      setCursorText(pendingText);\n      setPendingText(null);\n    }\n    if (!(pendingText || cursorText)) {\n      setTextWidth(0);\n    }\n  }, [pendingText, cursorText]);\n\n  // Handlers for child hover\n  const handleMouseOver = (e: React.MouseEvent) => {\n    const target = e.target as HTMLElement;\n    const text = target.getAttribute(\"data-cursor-text\");\n    if (text) {\n      setPendingText(text);\n    }\n  };\n  const handleMouseOut = () => {\n    setCursorText(null);\n    setPendingText(null);\n  };\n  const handleFocus = (e: React.FocusEvent) => {\n    const target = e.target as HTMLElement;\n    const text = target.getAttribute(\"data-cursor-text\");\n    if (text) {\n      setPendingText(text);\n    }\n  };\n  const handleBlur = () => {\n    setCursorText(null);\n    setPendingText(null);\n  };\n\n  return (\n    // biome-ignore lint/a11y/noNoninteractiveElementInteractions: Interactive cursor tracking widget requires mouse events\n    <div\n      className={`relative h-full w-full ${className}`}\n      onBlur={handleBlur}\n      onFocus={handleFocus}\n      onMouseOut={handleMouseOut}\n      onMouseOver={handleMouseOver}\n      role=\"application\"\n      style={{ cursor: \"none\", minHeight: 300 }}\n      // biome-ignore lint/a11y/noNoninteractiveTabindex: Interactive cursor tracking widget requires focus\n      tabIndex={0}\n    >\n      {children}\n      <motion.div\n        animate={\n          shouldReduceMotion\n            ? { opacity: 1, scale: 1 }\n            : {\n                opacity: 1,\n                scale: 1,\n                transition: {\n                  duration: 0.25,\n                  ease: [0.645, 0.045, 0.355, 1],\n                },\n              }\n        }\n        className=\"pointer-events-none fixed z-50\"\n        exit={shouldReduceMotion ? {} : { opacity: 0, scale: 0.7 }}\n        initial={\n          shouldReduceMotion\n            ? { opacity: 1, scale: 1 }\n            : { opacity: 0, scale: 0.7 }\n        }\n        style={{ left: 0, top: 0, x: springX, y: springY }}\n      >\n        <motion.div\n          animate={\n            cursorText\n              ? {\n                  background: \"var(--color-brand, #6366f1)\",\n                  borderRadius: 20,\n                  color: \"#fff\",\n                  height: 40,\n                  minHeight: 32,\n                  minWidth: 40,\n                  paddingLeft: 16,\n                  paddingRight: 16,\n                  scale: 1.1,\n                  width: bubbleWidth,\n                }\n              : {\n                  background: \"var(--color-brand, #6366f1)\",\n                  borderRadius: 999,\n                  color: \"#fff\",\n                  height: CIRCLE_SIZE,\n                  minHeight: CIRCLE_SIZE,\n                  minWidth: CIRCLE_SIZE,\n                  paddingLeft: 0,\n                  paddingRight: 0,\n                  scale: 1,\n                  width: CIRCLE_SIZE,\n                }\n          }\n          className=\"flex items-center justify-center font-medium text-xs shadow-lg\"\n          layout\n          style={{\n            alignItems: \"center\",\n            boxShadow: \"0 2px 8px 0 rgba(0,0,0,0.10)\",\n            display: \"flex\",\n            justifyContent: \"center\",\n            position: \"relative\",\n            zIndex: 1,\n          }}\n          transition={\n            shouldReduceMotion\n              ? { duration: 0 }\n              : { duration: 0.25, ease: [0.645, 0.045, 0.355, 1] }\n          }\n        >\n          {cursorText ? (\n            <motion.span\n              animate={{ filter: \"blur(0px)\", opacity: 1 }}\n              exit={{ filter: \"blur(8px)\", opacity: 0 }}\n              initial={{ filter: \"blur(8px)\", opacity: 0 }}\n              style={{\n                color: \"#fff\",\n                textAlign: \"center\",\n                whiteSpace: \"nowrap\",\n                width: \"100%\",\n              }}\n              transition={\n                shouldReduceMotion\n                  ? { duration: 0 }\n                  : {\n                      delay: 0.05,\n                      duration: 0.2,\n                      ease: [0.645, 0.045, 0.355, 1],\n                    }\n              }\n            >\n              {cursorText}\n            </motion.span>\n          ) : null}\n        </motion.div>\n        {/* Hidden span for pre-measuring text width */}\n        {pendingText || cursorText ? (\n          <span\n            ref={measureRef}\n            style={{\n              fontFamily: \"inherit\",\n              fontSize: \"0.75rem\",\n              fontWeight: 500,\n              paddingLeft: 16,\n              paddingRight: 16,\n              pointerEvents: \"none\",\n              position: \"absolute\",\n              visibility: \"hidden\",\n              whiteSpace: \"nowrap\",\n            }}\n          >\n            {pendingText || cursorText}\n          </span>\n        ) : null}\n      </motion.div>\n    </div>\n  );\n};\n\nexport default CursorFollow;\n","path":"index.tsx","target":"components/smoothui/cursor-follow/index.tsx","type":"registry:ui"},{"content":"import { useEffect, useState } from \"react\";\n\nexport function useCursorPosition() {\n  const [position, setPosition] = useState({ x: 0, y: 0 });\n\n  useEffect(() => {\n    const handleMouseMove = (event: MouseEvent) => {\n      setPosition({ x: event.clientX, y: event.clientY });\n    };\n    window.addEventListener(\"mousemove\", handleMouseMove);\n    return () => window.removeEventListener(\"mousemove\", handleMouseMove);\n  }, []);\n\n  return position;\n}\n","path":"use-cursor-position.tsx","target":"components/smoothui/cursor-follow/use-cursor-position.tsx","type":"registry:ui"}],"name":"cursor-follow","registryDependencies":["https://smoothui.dev/r/tokens.json"],"title":"Cursor Follow","type":"registry:ui"}