{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "creepy-button", "type": "registry:ui", "dependencies": [ "framer-motion" ], "files": [ { "path": "components/ui/creepy-button.tsx", "content": "\"use client\";\r\n\r\nimport React, { useRef, useState } from \"react\";\r\nimport { motion } from \"framer-motion\";\r\nimport { cn } from \"@/lib/utils\";\r\n\r\ninterface CreepyButtonProps extends React.ButtonHTMLAttributes {\r\n children: React.ReactNode;\r\n /**\r\n * Optional custom class for the button container\r\n */\r\n className?: string;\r\n /**\r\n * Optional custom class for the button cover (the visible part)\r\n */\r\n coverClassName?: string;\r\n}\r\n\r\ntype Coords = {\r\n x: number;\r\n y: number;\r\n};\r\n\r\nexport const CreepyButton = ({\r\n children,\r\n className,\r\n coverClassName,\r\n onClick,\r\n ...props\r\n}: CreepyButtonProps) => {\r\n const eyesRef = useRef(null);\r\n const [eyeCoords, setEyeCoords] = useState({ x: 0, y: 0 });\r\n const [isHovered, setIsHovered] = useState(false);\r\n\r\n const updateEyes = (e: React.MouseEvent | React.TouchEvent) => {\r\n const userEvent =\r\n \"touches\" in e ? (e as React.TouchEvent).touches[0] : (e as React.MouseEvent);\r\n\r\n if (!eyesRef.current) return;\r\n\r\n // get the center of the eyes container\r\n const eyesRect = eyesRef.current.getBoundingClientRect();\r\n const eyesCenter = {\r\n x: eyesRect.left + eyesRect.width / 2,\r\n y: eyesRect.top + eyesRect.height / 2,\r\n };\r\n\r\n // cursor position\r\n const cursor = {\r\n x: userEvent.clientX,\r\n y: userEvent.clientY,\r\n };\r\n\r\n // calculate the eye angle\r\n const dx = cursor.x - eyesCenter.x;\r\n const dy = cursor.y - eyesCenter.y;\r\n const angle = Math.atan2(-dy, dx) + Math.PI / 2;\r\n\r\n // pupil distance from the eye center\r\n const visionRangeX = 180; // Max distance to look horizontally\r\n const visionRangeY = 75; // Max distance to look vertically\r\n const distance = Math.hypot(dx, dy);\r\n\r\n // Limit the movement so pupils don't go too far\r\n // We normalize the distance influence\r\n const x = (Math.sin(angle) * Math.min(distance, visionRangeX)) / visionRangeX;\r\n const y = (Math.cos(angle) * Math.min(distance, visionRangeY)) / visionRangeY;\r\n\r\n setEyeCoords({ x, y });\r\n };\r\n\r\n // Reset eyes when mouse leaves\r\n const resetEyes = () => {\r\n setEyeCoords({ x: 0, y: 0 });\r\n setIsHovered(false);\r\n };\r\n\r\n const pupilStyle = {\r\n transform: `translate(calc(-50% + ${eyeCoords.x * 50}%), calc(-50% + ${eyeCoords.y * 50}%))`,\r\n };\r\n\r\n return (\r\n {\r\n updateEyes(e);\r\n setIsHovered(true);\r\n }}\r\n onTouchMove={updateEyes}\r\n onMouseLeave={resetEyes}\r\n onFocus={() => setIsHovered(true)}\r\n onBlur={() => setIsHovered(false)}\r\n {...props}\r\n >\r\n {/* Eyes Container */}\r\n \r\n {/* Left Eye */}\r\n \r\n \r\n \r\n {/* Right Eye */}\r\n \r\n \r\n \r\n \r\n\r\n {/* Button Cover */}\r\n \r\n {children}\r\n \r\n\r\n {/* Invisible placeholder to maintain size since cover is absolute */}\r\n \r\n {children}\r\n \r\n \r\n );\r\n};\r\n\r\nexport default CreepyButton;\r\n", "type": "registry:ui", "target": "components/ui/creepy-button.tsx" } ] }