{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "TargetCursor-JS-CSS",
	"title": "TargetCursor",
	"description": "A cursor follow animation with 4 corners that lock onto targets.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:file",
			"path": "TargetCursor.css",
			"target": "@components/TargetCursor.css",
			"content": ".target-cursor-wrapper {\n  position: fixed;\n  top: 0;\n  left: 0;\n  width: 0;\n  height: 0;\n  pointer-events: none;\n  z-index: 2147483647;\n  mix-blend-mode: difference;\n  transform: translate(-50%, -50%);\n}\n\n.target-cursor-dot {\n  position: absolute;\n  left: 50%;\n  top: 50%;\n  width: 4px;\n  height: 4px;\n  background: #fff;\n  border-radius: 50%;\n  transform: translate(-50%, -50%);\n  will-change: transform;\n}\n\n.target-cursor-corner {\n  position: absolute;\n  left: 50%;\n  top: 50%;\n  width: 12px;\n  height: 12px;\n  border: 3px solid #fff;\n  will-change: transform;\n}\n\n.corner-tl {\n  transform: translate(-150%, -150%);\n  border-right: none;\n  border-bottom: none;\n}\n\n.corner-tr {\n  transform: translate(50%, -150%);\n  border-left: none;\n  border-bottom: none;\n}\n\n.corner-br {\n  transform: translate(50%, 50%);\n  border-left: none;\n  border-top: none;\n}\n\n.corner-bl {\n  transform: translate(-150%, 50%);\n  border-right: none;\n  border-top: none;\n}\n"
		},
		{
			"type": "registry:component",
			"path": "TargetCursor.jsx",
			"content": "import { useEffect, useRef, useCallback, useMemo } from 'react';\nimport { createPortal } from 'react-dom';\nimport { gsap } from 'gsap';\nimport './TargetCursor.css';\n\n// A position: fixed element is positioned relative to the viewport UNLESS an\n// ancestor establishes a containing block (transform, perspective, filter,\n// will-change of those, or contain). When that happens, the cursor's translate\n// no longer maps to viewport coordinates, so we measure and compensate for it.\nconst getContainingBlock = element => {\n  let node = element?.parentElement;\n  while (node && node !== document.documentElement) {\n    const style = getComputedStyle(node);\n    if (\n      style.transform !== 'none' ||\n      style.perspective !== 'none' ||\n      style.filter !== 'none' ||\n      style.willChange.includes('transform') ||\n      style.willChange.includes('perspective') ||\n      style.willChange.includes('filter') ||\n      /paint|layout|strict|content/.test(style.contain)\n    ) {\n      return node;\n    }\n    node = node.parentElement;\n  }\n  return null;\n};\n\nconst getContainingBlockOffset = block => {\n  if (!block) return { x: 0, y: 0 };\n  const rect = block.getBoundingClientRect();\n  return { x: rect.left + block.clientLeft, y: rect.top + block.clientTop };\n};\n\nconst TargetCursor = ({\n  targetSelector = '.cursor-target',\n  spinDuration = 2,\n  hideDefaultCursor = true,\n  hoverDuration = 0.2,\n  parallaxOn = true,\n  cursorColor = '#ffffff',\n  cursorColorOnTarget\n}) => {\n  const cursorRef = useRef(null);\n  const cornersRef = useRef(null);\n  const spinTl = useRef(null);\n  const dotRef = useRef(null);\n  const containingBlockRef = useRef(null);\n\n  const isActiveRef = useRef(false);\n  const targetCornerPositionsRef = useRef(null);\n  const tickerFnRef = useRef(null);\n  const activeStrengthRef = useRef(0);\n\n  const isMobile = useMemo(() => {\n    if (typeof window === 'undefined') return false;\n    const hasTouchScreen = 'ontouchstart' in window || navigator.maxTouchPoints > 0;\n    const isSmallScreen = window.innerWidth <= 768;\n    const userAgent = navigator.userAgent || navigator.vendor || window.opera;\n    const mobileRegex = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i;\n    const isMobileUserAgent = mobileRegex.test(userAgent.toLowerCase());\n    return (hasTouchScreen && isSmallScreen) || isMobileUserAgent;\n  }, []);\n\n  const constants = useMemo(\n    () => ({\n      borderWidth: 3,\n      cornerSize: 12\n    }),\n    []\n  );\n\n  const moveCursor = useCallback((x, y) => {\n    if (!cursorRef.current) return;\n    const { x: offsetX, y: offsetY } = getContainingBlockOffset(containingBlockRef.current);\n    gsap.to(cursorRef.current, {\n      x: x - offsetX,\n      y: y - offsetY,\n      duration: 0.1,\n      ease: 'power3.out'\n    });\n  }, []);\n\n  useEffect(() => {\n    if (isMobile || !cursorRef.current) return;\n\n    const originalCursor = document.body.style.cursor;\n    if (hideDefaultCursor) {\n      document.body.style.cursor = 'none';\n    }\n\n    const cursor = cursorRef.current;\n    cornersRef.current = cursor.querySelectorAll('.target-cursor-corner');\n\n    containingBlockRef.current = getContainingBlock(cursor);\n    const getOffset = () => getContainingBlockOffset(containingBlockRef.current);\n\n    let activeTarget = null;\n    let currentLeaveHandler = null;\n    let resumeTimeout = null;\n\n    const cleanupTarget = target => {\n      if (currentLeaveHandler) {\n        target.removeEventListener('mouseleave', currentLeaveHandler);\n      }\n      currentLeaveHandler = null;\n    };\n\n    const initialOffset = getOffset();\n    gsap.set(cursor, {\n      xPercent: -50,\n      yPercent: -50,\n      x: window.innerWidth / 2 - initialOffset.x,\n      y: window.innerHeight / 2 - initialOffset.y\n    });\n\n    const createSpinTimeline = () => {\n      if (spinTl.current) {\n        spinTl.current.kill();\n      }\n      spinTl.current = gsap\n        .timeline({ repeat: -1 })\n        .to(cursor, { rotation: '+=360', duration: spinDuration, ease: 'none' });\n    };\n\n    createSpinTimeline();\n\n    const tickerFn = () => {\n      if (!targetCornerPositionsRef.current || !cursorRef.current || !cornersRef.current) {\n        return;\n      }\n\n      const strength = activeStrengthRef.current;\n      if (strength === 0) return;\n\n      const cursorX = gsap.getProperty(cursorRef.current, 'x');\n      const cursorY = gsap.getProperty(cursorRef.current, 'y');\n\n      const corners = Array.from(cornersRef.current);\n      corners.forEach((corner, i) => {\n        const currentX = gsap.getProperty(corner, 'x');\n        const currentY = gsap.getProperty(corner, 'y');\n\n        const targetX = targetCornerPositionsRef.current[i].x - cursorX;\n        const targetY = targetCornerPositionsRef.current[i].y - cursorY;\n\n        const finalX = currentX + (targetX - currentX) * strength;\n        const finalY = currentY + (targetY - currentY) * strength;\n\n        const duration = strength >= 0.99 ? (parallaxOn ? 0.2 : 0) : 0.05;\n\n        gsap.to(corner, {\n          x: finalX,\n          y: finalY,\n          duration: duration,\n          ease: duration === 0 ? 'none' : 'power1.out',\n          overwrite: 'auto'\n        });\n      });\n    };\n\n    tickerFnRef.current = tickerFn;\n\n    const moveHandler = e => moveCursor(e.clientX, e.clientY);\n    window.addEventListener('mousemove', moveHandler);\n\n    const scrollHandler = () => {\n      if (!activeTarget || !cursorRef.current) return;\n      const { x: offsetX, y: offsetY } = getOffset();\n      const mouseX = gsap.getProperty(cursorRef.current, 'x') + offsetX;\n      const mouseY = gsap.getProperty(cursorRef.current, 'y') + offsetY;\n      const elementUnderMouse = document.elementFromPoint(mouseX, mouseY);\n      const isStillOverTarget =\n        elementUnderMouse &&\n        (elementUnderMouse === activeTarget || elementUnderMouse.closest(targetSelector) === activeTarget);\n      if (!isStillOverTarget) {\n        if (currentLeaveHandler) {\n          currentLeaveHandler();\n        }\n      }\n    };\n    window.addEventListener('scroll', scrollHandler, { passive: true });\n\n    const mouseDownHandler = () => {\n      if (!dotRef.current) return;\n      gsap.to(dotRef.current, { scale: 0.7, duration: 0.3 });\n      gsap.to(cursorRef.current, { scale: 0.9, duration: 0.2 });\n    };\n\n    const mouseUpHandler = () => {\n      if (!dotRef.current) return;\n      gsap.to(dotRef.current, { scale: 1, duration: 0.3 });\n      gsap.to(cursorRef.current, { scale: 1, duration: 0.2 });\n    };\n\n    window.addEventListener('mousedown', mouseDownHandler);\n    window.addEventListener('mouseup', mouseUpHandler);\n\n    const enterHandler = e => {\n      const directTarget = e.target;\n      const allTargets = [];\n      let current = directTarget;\n      while (current && current !== document.body) {\n        if (current.matches(targetSelector)) {\n          allTargets.push(current);\n        }\n        current = current.parentElement;\n      }\n      const target = allTargets[0] || null;\n      if (!target || !cursorRef.current || !cornersRef.current) return;\n      if (activeTarget === target) return;\n      if (activeTarget) {\n        cleanupTarget(activeTarget);\n      }\n      if (resumeTimeout) {\n        clearTimeout(resumeTimeout);\n        resumeTimeout = null;\n      }\n\n      activeTarget = target;\n      const corners = Array.from(cornersRef.current);\n      corners.forEach(corner => gsap.killTweensOf(corner, 'x,y'));\n\n      gsap.killTweensOf(cursorRef.current, 'rotation');\n      spinTl.current?.pause();\n      gsap.set(cursorRef.current, { rotation: 0 });\n\n      if (cursorColorOnTarget) {\n        gsap.to(corners, {\n          borderColor: cursorColorOnTarget,\n          duration: 0.15,\n          ease: 'power2.out'\n        });\n        if (dotRef.current) {\n          gsap.to(dotRef.current, {\n            backgroundColor: cursorColorOnTarget,\n            duration: 0.15,\n            ease: 'power2.out'\n          });\n        }\n      }\n\n      const rect = target.getBoundingClientRect();\n      const { borderWidth, cornerSize } = constants;\n      const { x: offsetX, y: offsetY } = getOffset();\n      const cursorX = gsap.getProperty(cursorRef.current, 'x');\n      const cursorY = gsap.getProperty(cursorRef.current, 'y');\n\n      targetCornerPositionsRef.current = [\n        { x: rect.left - borderWidth - offsetX, y: rect.top - borderWidth - offsetY },\n        { x: rect.right + borderWidth - cornerSize - offsetX, y: rect.top - borderWidth - offsetY },\n        { x: rect.right + borderWidth - cornerSize - offsetX, y: rect.bottom + borderWidth - cornerSize - offsetY },\n        { x: rect.left - borderWidth - offsetX, y: rect.bottom + borderWidth - cornerSize - offsetY }\n      ];\n\n      isActiveRef.current = true;\n      gsap.ticker.add(tickerFnRef.current);\n\n      gsap.to(activeStrengthRef, {\n        current: 1,\n        duration: hoverDuration,\n        ease: 'power2.out'\n      });\n\n      corners.forEach((corner, i) => {\n        gsap.to(corner, {\n          x: targetCornerPositionsRef.current[i].x - cursorX,\n          y: targetCornerPositionsRef.current[i].y - cursorY,\n          duration: 0.2,\n          ease: 'power2.out'\n        });\n      });\n\n      const leaveHandler = () => {\n        gsap.ticker.remove(tickerFnRef.current);\n\n        isActiveRef.current = false;\n        targetCornerPositionsRef.current = null;\n        gsap.set(activeStrengthRef, { current: 0, overwrite: true });\n        activeTarget = null;\n\n        if (cursorColorOnTarget && cornersRef.current) {\n          gsap.to(Array.from(cornersRef.current), {\n            borderColor: cursorColor,\n            duration: 0.15,\n            ease: 'power2.out'\n          });\n          if (dotRef.current) {\n            gsap.to(dotRef.current, {\n              backgroundColor: cursorColor,\n              duration: 0.15,\n              ease: 'power2.out'\n            });\n          }\n        }\n\n        if (cornersRef.current) {\n          const corners = Array.from(cornersRef.current);\n          gsap.killTweensOf(corners, 'x,y');\n          const { cornerSize } = constants;\n          const positions = [\n            { x: -cornerSize * 1.5, y: -cornerSize * 1.5 },\n            { x: cornerSize * 0.5, y: -cornerSize * 1.5 },\n            { x: cornerSize * 0.5, y: cornerSize * 0.5 },\n            { x: -cornerSize * 1.5, y: cornerSize * 0.5 }\n          ];\n          const tl = gsap.timeline();\n          corners.forEach((corner, index) => {\n            tl.to(\n              corner,\n              {\n                x: positions[index].x,\n                y: positions[index].y,\n                duration: 0.3,\n                ease: 'power3.out'\n              },\n              0\n            );\n          });\n        }\n\n        resumeTimeout = setTimeout(() => {\n          if (!activeTarget && cursorRef.current && spinTl.current) {\n            const currentRotation = gsap.getProperty(cursorRef.current, 'rotation');\n            const normalizedRotation = currentRotation % 360;\n            spinTl.current.kill();\n            spinTl.current = gsap\n              .timeline({ repeat: -1 })\n              .to(cursorRef.current, { rotation: '+=360', duration: spinDuration, ease: 'none' });\n            gsap.to(cursorRef.current, {\n              rotation: normalizedRotation + 360,\n              duration: spinDuration * (1 - normalizedRotation / 360),\n              ease: 'none',\n              onComplete: () => {\n                spinTl.current?.restart();\n              }\n            });\n          }\n          resumeTimeout = null;\n        }, 50);\n\n        cleanupTarget(target);\n      };\n\n      currentLeaveHandler = leaveHandler;\n      target.addEventListener('mouseleave', leaveHandler);\n    };\n\n    window.addEventListener('mouseover', enterHandler, { passive: true });\n\n    const resizeHandler = () => {\n      containingBlockRef.current = getContainingBlock(cursor);\n    };\n    window.addEventListener('resize', resizeHandler);\n\n    return () => {\n      if (tickerFnRef.current) {\n        gsap.ticker.remove(tickerFnRef.current);\n      }\n\n      window.removeEventListener('mousemove', moveHandler);\n      window.removeEventListener('mouseover', enterHandler);\n      window.removeEventListener('scroll', scrollHandler);\n      window.removeEventListener('resize', resizeHandler);\n      window.removeEventListener('mousedown', mouseDownHandler);\n      window.removeEventListener('mouseup', mouseUpHandler);\n\n      if (activeTarget) {\n        cleanupTarget(activeTarget);\n      }\n\n      spinTl.current?.kill();\n      document.body.style.cursor = originalCursor;\n\n      isActiveRef.current = false;\n      targetCornerPositionsRef.current = null;\n      activeStrengthRef.current = 0;\n    };\n  }, [\n    targetSelector,\n    spinDuration,\n    moveCursor,\n    constants,\n    hideDefaultCursor,\n    isMobile,\n    hoverDuration,\n    parallaxOn,\n    cursorColor,\n    cursorColorOnTarget\n  ]);\n\n  useEffect(() => {\n    if (isMobile || !cursorRef.current || !spinTl.current) return;\n    if (spinTl.current.isActive()) {\n      spinTl.current.kill();\n      spinTl.current = gsap\n        .timeline({ repeat: -1 })\n        .to(cursorRef.current, { rotation: '+=360', duration: spinDuration, ease: 'none' });\n    }\n  }, [spinDuration, isMobile]);\n\n  if (isMobile || typeof document === 'undefined') {\n    return null;\n  }\n\n  return createPortal(\n    <div ref={cursorRef} className=\"target-cursor-wrapper\">\n      <div ref={dotRef} className=\"target-cursor-dot\" style={{ backgroundColor: cursorColor }} />\n      <div className=\"target-cursor-corner corner-tl\" style={{ borderColor: cursorColor }} />\n      <div className=\"target-cursor-corner corner-tr\" style={{ borderColor: cursorColor }} />\n      <div className=\"target-cursor-corner corner-br\" style={{ borderColor: cursorColor }} />\n      <div className=\"target-cursor-corner corner-bl\" style={{ borderColor: cursorColor }} />\n    </div>,\n    document.body\n  );\n};\n\nexport default TargetCursor;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"react-dom@^19.0.0",
		"gsap@^3.13.0"
	]
}