Created
December 3, 2025 13:23
-
-
Save divv919/11be9e5185e2cef173c6a486595502b5 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| "use client"; | |
| import { animate } from "motion"; | |
| import { | |
| motion, | |
| MotionValue, | |
| useMotionTemplate, | |
| useMotionValue, | |
| useTransform, | |
| } from "motion/react"; | |
| import { useRef, useState } from "react"; | |
| type Position = { | |
| left: string; | |
| top: string; | |
| }; | |
| type CircleRingProps = { | |
| radius: number; | |
| className?: string; | |
| }; | |
| type DotLayerProps = { | |
| id: string; | |
| positions: Position[]; | |
| zIndex?: string; | |
| }; | |
| type SpotlightProps = { | |
| rotation: MotionValue<number>; | |
| }; | |
| const RING_RADII = [120, 200, 280, 360] as const; | |
| const RING_STYLES = [ | |
| "bg-neutral-700/20 z-100", | |
| "bg-neutral-700/16 z-10", | |
| "bg-neutral-700/10", | |
| "opacity-60", | |
| ] as const; | |
| const PRIMARY_TARGET_POSITIONS: Position[] = [ | |
| { left: "59.5%", top: "18.5%" }, | |
| { left: "40%", top: "18.5%" }, | |
| { left: "48%", top: "41%" }, | |
| { left: "25%", top: "41%" }, | |
| { left: "65%", top: "52%" }, | |
| { left: "80%", top: "29%" }, | |
| { left: "45%", top: "74%" }, | |
| ]; | |
| const SECONDARY_TARGET_POSITIONS: Position[] = [ | |
| { left: "50%", top: "24.5%" }, | |
| { left: "33%", top: "30.5%" }, | |
| { left: "53%", top: "41%" }, | |
| { left: "45%", top: "57.2%" }, | |
| { left: "67%", top: "50.2%" }, | |
| { left: "63%", top: "71%" }, | |
| ]; | |
| const LAYER_IDS = { | |
| primary: "layer-primary", | |
| secondary: "layer-secondary", | |
| } as const; | |
| const ANIMATION_DURATION = { | |
| fade: 0.3, | |
| move: 0.4, | |
| expand: 0.6, | |
| contract: 0.6, | |
| } as const; | |
| const AIM_ROTATION_INPUT = [0, 100] as number[]; | |
| const AIM_ROTATION_OUTPUT = [45, -45] as number[]; | |
| export default function TargetingSystem() { | |
| return ( | |
| <div className="w-full h-full flex items-center justify-center "> | |
| <TargetingCard /> | |
| </div> | |
| ); | |
| } | |
| function TargetingCard() { | |
| const [isAnimating, setIsAnimating] = useState(false); | |
| const isHoveredRef = useRef(false); | |
| const activeLayerRef = useRef<"primary" | "secondary">("primary"); | |
| const aimXPosition = useMotionValue(50); | |
| const spotlightRotation = useTransform<number, number>( | |
| aimXPosition, | |
| AIM_ROTATION_INPUT, | |
| AIM_ROTATION_OUTPUT | |
| ); | |
| const getRandomPosition = (positions: Position[]) => | |
| positions[Math.floor(positions.length * Math.random())]; | |
| const getLayerPositions = (layer: "primary" | "secondary") => | |
| layer === "primary" ? PRIMARY_TARGET_POSITIONS : SECONDARY_TARGET_POSITIONS; | |
| const getLayerId = (layer: "primary" | "secondary") => | |
| layer === "primary" ? LAYER_IDS.primary : LAYER_IDS.secondary; | |
| const getOppositeLayer = (layer: "primary" | "secondary") => | |
| layer === "primary" ? "secondary" : "primary"; | |
| async function animateSingleCycle() { | |
| const currentLayer = activeLayerRef.current; | |
| const oppositeLayer = getOppositeLayer(currentLayer); | |
| const targetPosition = getRandomPosition(getLayerPositions(currentLayer)); | |
| await animate( | |
| `#${getLayerId(oppositeLayer)}`, | |
| { opacity: 0 }, | |
| { duration: ANIMATION_DURATION.fade } | |
| ); | |
| animate( | |
| `#${getLayerId(currentLayer)}`, | |
| { opacity: 1 }, | |
| { duration: ANIMATION_DURATION.fade } | |
| ); | |
| animate("#target-indicator", { opacity: 0 }, { duration: 0.18 }); | |
| const repositionDelayMs = 300; | |
| setTimeout(() => { | |
| animate( | |
| "#target-indicator", | |
| { top: targetPosition.top, left: targetPosition.left }, | |
| { duration: 0 } | |
| ); | |
| animate("#target-indicator", { opacity: 1 }, { duration: 0.25 }); | |
| }, repositionDelayMs); | |
| const xValue = parseFloat(targetPosition.left); | |
| animate(aimXPosition, xValue, { duration: ANIMATION_DURATION.move }); | |
| await animate( | |
| "#aim-reticle", | |
| { top: targetPosition.top, left: targetPosition.left }, | |
| { duration: ANIMATION_DURATION.move } | |
| ); | |
| await animate( | |
| "#reticle-circle", | |
| { strokeDasharray: "21 0", r: 14, strokeDashoffset: 0 }, | |
| { duration: ANIMATION_DURATION.expand } | |
| ); | |
| await animate( | |
| "#reticle-circle", | |
| { strokeDasharray: "11 3.1", r: 9, strokeDashoffset: 0 }, | |
| { duration: ANIMATION_DURATION.contract, delay: 0.5 } | |
| ); | |
| activeLayerRef.current = oppositeLayer; | |
| } | |
| async function startAnimationLoop() { | |
| animate("#targeting-card", { filter: "saturate(100%)" }); | |
| while (isHoveredRef.current) { | |
| await animateSingleCycle(); | |
| } | |
| setIsAnimating(false); | |
| animate("#targeting-card", { filter: "saturate(0%)" }); | |
| } | |
| const handleHoverStart = () => { | |
| isHoveredRef.current = true; | |
| if (!isAnimating) { | |
| setIsAnimating(true); | |
| startAnimationLoop(); | |
| } | |
| }; | |
| const handleHoverEnd = () => { | |
| isHoveredRef.current = false; | |
| }; | |
| return ( | |
| <motion.div | |
| id="targeting-card" | |
| onHoverStart={handleHoverStart} | |
| onHoverEnd={handleHoverEnd} | |
| initial={{ filter: "saturate(0%)" }} | |
| className="w-200 h-120 rounded-3xl bg-neutral-800 relative overflow-hidden shadow-xl" | |
| > | |
| <EdgeShadow position="left" /> | |
| <EdgeShadow position="right" /> | |
| {RING_RADII.map((radius, index) => ( | |
| <CircleRing | |
| key={radius} | |
| radius={radius} | |
| className={RING_STYLES[index]} | |
| /> | |
| ))} | |
| <DotLayer | |
| id={LAYER_IDS.primary} | |
| positions={PRIMARY_TARGET_POSITIONS} | |
| zIndex="z-100" | |
| /> | |
| <DotLayer | |
| id={LAYER_IDS.secondary} | |
| positions={SECONDARY_TARGET_POSITIONS} | |
| zIndex="z-120" | |
| /> | |
| <AimReticle /> | |
| <TargetIndicator /> | |
| <Spotlight rotation={spotlightRotation} /> | |
| </motion.div> | |
| ); | |
| } | |
| function EdgeShadow({ position }: { position: "left" | "right" }) { | |
| const positionClasses = | |
| position === "left" | |
| ? "left-0 rotate-120 -translate-x-[50%]" | |
| : "right-0 rotate-60 translate-x-[50%]"; | |
| return ( | |
| <div | |
| className={`h-100 w-250 bg-neutral-800/100 opacity-70 absolute z-100 ${positionClasses} blur-xl -translate-y-[40%]`} | |
| /> | |
| ); | |
| } | |
| function CircleRing({ radius, className }: CircleRingProps) { | |
| const diameter = radius * 2; | |
| return ( | |
| <svg | |
| width={diameter} | |
| height={diameter} | |
| viewBox={`0 0 ${diameter} ${diameter}`} | |
| className={`absolute left-1/2 -translate-x-1/2 top-0 -translate-y-1/2 shadow-[0px_20px_20px_rgba(37,37,37,1)] rounded-full ${ | |
| className ?? "" | |
| }`} | |
| > | |
| <circle | |
| cx={radius} | |
| cy={radius} | |
| r={radius} | |
| fill="none" | |
| stroke="rgb(163 163 163 / 0.2)" | |
| strokeWidth={2} | |
| strokeDasharray="2 6" | |
| /> | |
| </svg> | |
| ); | |
| } | |
| function DotLayer({ id, positions, zIndex = "z-100" }: DotLayerProps) { | |
| return ( | |
| <motion.div | |
| id={id} | |
| initial={{ opacity: 0 }} | |
| className={`w-full h-full ${zIndex} absolute`} | |
| > | |
| <div className="relative w-full h-full"> | |
| {positions.map((position, index) => ( | |
| <div | |
| key={`${id}-dot-${index}`} | |
| className="size-[6px] shadow-3xl absolute bg-neutral-400/60 rounded-full" | |
| style={{ left: position.left, top: position.top }} | |
| /> | |
| ))} | |
| </div> | |
| </motion.div> | |
| ); | |
| } | |
| function AimReticle() { | |
| return ( | |
| <div | |
| id="aim-reticle" | |
| className="w-fit h-fit absolute top-[74%] left-[45%] -translate-y-[40%] -translate-x-[38%] rotate-8 border-2 border-neutral-700 bg-neutral-900/40 shadow-[0px_10px_10px_rgba(0,0,0,0.18)] rounded-full z-100" | |
| > | |
| <svg width={30} height={30} viewBox="0 0 30 30" className="scale-113"> | |
| <motion.circle | |
| id="reticle-circle" | |
| initial={{ strokeDasharray: "11 3.1", r: 9, strokeDashoffset: 0 }} | |
| transition={{ | |
| duration: 0.4, | |
| repeat: Infinity, | |
| repeatType: "reverse", | |
| repeatDelay: 1, | |
| }} | |
| fill="none" | |
| stroke="#5de3ff" | |
| strokeWidth={2} | |
| cx={15} | |
| cy={15} | |
| /> | |
| </svg> | |
| </div> | |
| ); | |
| } | |
| function TargetIndicator() { | |
| return ( | |
| <motion.div | |
| id="target-indicator" | |
| initial={{ opacity: 0 }} | |
| className="absolute h-[10px] w-[9px] rounded-[2px] bg-[#5de3ff] top-[41%] left-[10%] -translate-y-1/6 z-100 shadow-[0px_0px_14px_rgba(93,227,255,0.9),inset_0px_2px_3px_rgba(255,255,255,0.7)]" | |
| /> | |
| ); | |
| } | |
| function Spotlight({ rotation }: SpotlightProps) { | |
| const rotationStyle = useMotionTemplate`${rotation}deg`; | |
| return ( | |
| <motion.svg | |
| data-spotlight="true" | |
| viewBox="0 0 232 168" | |
| aria-hidden="true" | |
| className="absolute left-1/2 top-0 -translate-y-[20%] -translate-x-1/2 mix-blend-overlay blur-lg" | |
| style={{ | |
| top: "0", | |
| width: "500px", | |
| transformOrigin: "50% -4.0625rem", | |
| rotate: rotationStyle, | |
| }} | |
| > | |
| <path fill="url(#spotlight-gradient)" d="M85-11h62l85 179H0L85-11Z" /> | |
| <defs> | |
| <linearGradient | |
| id="spotlight-gradient" | |
| x1="116" | |
| x2="116" | |
| y1="5" | |
| y2="168" | |
| gradientUnits="userSpaceOnUse" | |
| > | |
| <stop stopColor="#fff" /> | |
| <stop offset="1" stopColor="#fff" stopOpacity="0" /> | |
| </linearGradient> | |
| </defs> | |
| </motion.svg> | |
| ); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment