{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "custom-cursor",
  "title": "Custom Cursor",
  "description": "A smooth custom cursor that morphs when hovering interactive targets, powered by Motion.",
  "dependencies": ["motion", "class-variance-authority"],
  "registryDependencies": ["utils"],
  "files": [
    {
      "path": "registry/primitives/effects/custom-cursor/index.tsx",
      "content": "/** biome-ignore-all lint/a11y/noNoninteractiveElementInteractions: Cursor targets use hover-only affordances. */\n/** biome-ignore-all lint/a11y/noStaticElementInteractions: Cursor targets are decorative hover zones. */\n\n\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport {\n  motion,\n  type SpringOptions,\n  useMotionValue,\n  useReducedMotion,\n  useSpring,\n} from \"motion/react\";\nimport {\n  type ComponentProps,\n  type ComponentPropsWithoutRef,\n  createContext,\n  type ReactNode,\n  useContext,\n  useEffect,\n  useMemo,\n  useState,\n} from \"react\";\n\ntype MotionDivAnimate = NonNullable<\n  ComponentProps<typeof motion.div>[\"animate\"]\n>;\n\nconst DEFAULT_CURSOR_COLOR = \"#ff4c24\";\n\nconst CURSOR_EASE = [0.625, 0.05, 0, 1] as const;\n\nconst customCursorVariants = cva(\"relative select-none\", {\n  variants: {\n    layout: {\n      default: \"\",\n      demo: \"flex min-h-72 items-center justify-center\",\n    },\n  },\n  defaultVariants: {\n    layout: \"default\",\n  },\n});\n\nconst customCursorTargetVariants = cva(\n  \"flex items-center justify-center text-foreground transition-opacity hover:opacity-80\",\n  {\n    variants: {\n      size: {\n        sm: \"size-10\",\n        md: \"size-14\",\n        lg: \"size-16\",\n      },\n    },\n    defaultVariants: {\n      size: \"md\",\n    },\n  }\n);\n\ninterface CustomCursorContextValue {\n  setIsHovering: (isHovering: boolean) => void;\n}\n\nconst CustomCursorContext = createContext<CustomCursorContextValue | null>(\n  null\n);\n\nfunction useCustomCursorContext() {\n  const context = useContext(CustomCursorContext);\n\n  if (!context) {\n    throw new Error(\n      \"CustomCursorTarget must be used within a CustomCursor provider.\"\n    );\n  }\n\n  return context;\n}\n\nfunction hexToRgba(hex: string, alpha: number) {\n  const normalized = hex.replace(\"#\", \"\");\n\n  if (normalized.length !== 6) {\n    return `rgba(255, 76, 36, ${alpha})`;\n  }\n\n  const red = Number.parseInt(normalized.slice(0, 2), 16);\n  const green = Number.parseInt(normalized.slice(2, 4), 16);\n  const blue = Number.parseInt(normalized.slice(4, 6), 16);\n\n  return `rgba(${red}, ${green}, ${blue}, ${alpha})`;\n}\n\nfunction resolveCursorAppearance(\n  isHovering: boolean,\n  color: string\n): MotionDivAnimate {\n  if (!isHovering) {\n    return {\n      width: 16,\n      height: 16,\n      borderRadius: 9999,\n      backgroundColor: color,\n      borderColor: color,\n      borderWidth: 1,\n    };\n  }\n\n  return {\n    width: 48,\n    height: 48,\n    borderRadius: 9999,\n    backgroundColor: hexToRgba(color, 0.3),\n    borderColor: color,\n    borderWidth: 1,\n  };\n}\n\nfunction useCoarsePointer() {\n  const [isCoarsePointer, setIsCoarsePointer] = useState(false);\n\n  useEffect(() => {\n    const mediaQuery = window.matchMedia(\"(pointer: coarse)\");\n\n    const update = () => {\n      setIsCoarsePointer(mediaQuery.matches);\n    };\n\n    update();\n    mediaQuery.addEventListener(\"change\", update);\n\n    return () => {\n      mediaQuery.removeEventListener(\"change\", update);\n    };\n  }, []);\n\n  return isCoarsePointer;\n}\n\nexport interface CustomCursorProps\n  extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\">,\n    VariantProps<typeof customCursorVariants> {\n  children?: ReactNode;\n  /** Cursor fill and border color. */\n  color?: string;\n  /** Spring damping for pointer follow. */\n  followDamping?: number;\n  /** Spring stiffness for pointer follow. */\n  followStiffness?: number;\n  /** Override spring options for pointer follow. */\n  followTransition?: SpringOptions;\n}\n\nfunction CustomCursor({\n  children,\n  className,\n  color = DEFAULT_CURSOR_COLOR,\n  followDamping = 22,\n  followStiffness = 150,\n  followTransition,\n  layout = \"default\",\n  ...props\n}: CustomCursorProps) {\n  const prefersReducedMotion = useReducedMotion();\n  const isCoarsePointer = useCoarsePointer();\n  const [isHovering, setIsHovering] = useState(false);\n\n  const cursorX = useMotionValue(0);\n  const cursorY = useMotionValue(0);\n\n  const springTransition = followTransition ?? {\n    damping: followDamping,\n    stiffness: followStiffness,\n    mass: 0.8,\n  };\n\n  const springX = useSpring(cursorX, springTransition);\n  const springY = useSpring(cursorY, springTransition);\n\n  const contextValue = useMemo<CustomCursorContextValue>(\n    () => ({\n      setIsHovering,\n    }),\n    []\n  );\n\n  useEffect(() => {\n    if (isCoarsePointer || prefersReducedMotion) {\n      return;\n    }\n\n    const handlePointerMove = (event: PointerEvent) => {\n      cursorX.set(event.clientX);\n      cursorY.set(event.clientY);\n    };\n\n    window.addEventListener(\"pointermove\", handlePointerMove);\n\n    return () => {\n      window.removeEventListener(\"pointermove\", handlePointerMove);\n    };\n  }, [cursorX, cursorY, isCoarsePointer, prefersReducedMotion]);\n\n  const appearance = resolveCursorAppearance(isHovering, color);\n\n  return (\n    <CustomCursorContext.Provider value={contextValue}>\n      <div\n        className={cn(customCursorVariants({ layout, className }))}\n        {...props}\n      >\n        {isCoarsePointer || prefersReducedMotion ? null : (\n          <motion.div\n            animate={appearance}\n            aria-hidden=\"true\"\n            className=\"pointer-events-none fixed top-0 left-0 z-[100] border\"\n            initial={false}\n            style={{\n              x: springX,\n              y: springY,\n              translateX: \"-50%\",\n              translateY: \"-50%\",\n            }}\n            transition={{\n              duration: 0.375,\n              ease: CURSOR_EASE,\n            }}\n          />\n        )}\n        {children}\n      </div>\n    </CustomCursorContext.Provider>\n  );\n}\n\nexport interface CustomCursorTargetProps\n  extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\">,\n    VariantProps<typeof customCursorTargetVariants> {\n  children?: ReactNode;\n}\n\nfunction CustomCursorTarget({\n  children,\n  className,\n  size,\n  ...props\n}: CustomCursorTargetProps) {\n  const { setIsHovering } = useCustomCursorContext();\n\n  return (\n    <div\n      className={cn(customCursorTargetVariants({ size, className }))}\n      data-cursor=\"\"\n      onMouseEnter={() => {\n        setIsHovering(true);\n      }}\n      onMouseLeave={() => {\n        setIsHovering(false);\n      }}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport {\n  CustomCursor,\n  CustomCursorTarget,\n  customCursorTargetVariants,\n  customCursorVariants,\n};\n",
      "type": "registry:ui",
      "target": "components/sora-ui/effects/custom-cursor.tsx"
    }
  ],
  "type": "registry:ui"
}
