diff --git a/app/globals.css b/app/globals.css index d112e931..083f6ee9 100644 --- a/app/globals.css +++ b/app/globals.css @@ -194,6 +194,37 @@ pre[class*="language-"] { user-select: none; } +@layer components { + .spotlight-card { + --spotlight-x: 50%; + --spotlight-y: 50%; + --spotlight-color: rgba(59, 130, 246, 0.15); + } + + .dark .spotlight-card { + --spotlight-color: rgba(59, 130, 246, 0.25); + } + + .spotlight-card::after { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + pointer-events: none; + background: radial-gradient( + 600px circle at var(--spotlight-x) var(--spotlight-y), + var(--spotlight-color), + transparent 60% + ); + opacity: 0; + transition: opacity 0.3s ease; + } + + .spotlight-card:hover::after { + opacity: 1; + } +} + /* Sponsor carousel scroll animation - GPU accelerated */ @keyframes scroll { from { diff --git a/app/notification-generator/page.tsx b/app/notification-generator/page.tsx index d1245b4b..e70a3ffe 100644 --- a/app/notification-generator/page.tsx +++ b/app/notification-generator/page.tsx @@ -6,6 +6,7 @@ import { useCallback, useEffect, useState } from "react"; import type { NotificationConfig } from "@/components/notification-generator/types"; import { Button } from "@/components/ui/button"; import { FadeIn, MotionSection, SlideIn } from "@/components/ui/motion/motion-components"; +import { useSpotlight } from "@/hooks/use-spotlight"; const NotificationGeneratedCode = dynamic( () => @@ -47,6 +48,8 @@ export default function NotificationGeneratorPage() { const [yamlCode, setYamlCode] = useState(""); const [previewKey, setPreviewKey] = useState(0); + const configSpotlight = useSpotlight(); + const previewSpotlight = useSpotlight(); const generateYaml = useCallback(() => generateYamlString(notification), [notification]); @@ -82,7 +85,11 @@ export default function NotificationGeneratorPage() { -
+

Generated Configuration

@@ -92,7 +99,11 @@ export default function NotificationGeneratorPage() {
-
+

Live Preview

diff --git a/app/privacy-policy/page.tsx b/app/privacy-policy/page.tsx index 168b960c..28fa91c1 100644 --- a/app/privacy-policy/page.tsx +++ b/app/privacy-policy/page.tsx @@ -19,9 +19,11 @@ import { MotionSection, SlideIn, } from "@/components/ui/motion/motion-components"; +import { useSpotlight } from "@/hooks/use-spotlight"; export default function PrivacyPolicyPage() { const lastUpdated = "December 9, 2025"; + const spotlight = useSpotlight(); return (
@@ -57,7 +59,11 @@ export default function PrivacyPolicyPage() { {/* Who We Are */} -
+
@@ -89,7 +95,11 @@ export default function PrivacyPolicyPage() {
-
+

Cookies @@ -102,7 +112,11 @@ export default function PrivacyPolicyPage() { -
+

Analytics @@ -115,7 +129,11 @@ export default function PrivacyPolicyPage() { -
+

Communications @@ -132,7 +150,11 @@ export default function PrivacyPolicyPage() { {/* Usage & Cookie Details - Split View */}
-
+

Data Usage

@@ -157,7 +179,11 @@ export default function PrivacyPolicyPage() { -
+

Cookie Types

@@ -194,7 +220,11 @@ export default function PrivacyPolicyPage() { {/* User Rights */} -
+
@@ -216,7 +246,11 @@ export default function PrivacyPolicyPage() { {/* Contact Section */} -
+

Contact Us @@ -246,7 +280,11 @@ export default function PrivacyPolicyPage() {

-
+

Important Note

diff --git a/app/projects/eternalcombat/page.tsx b/app/projects/eternalcombat/page.tsx index e8f27cdc..953e5132 100644 --- a/app/projects/eternalcombat/page.tsx +++ b/app/projects/eternalcombat/page.tsx @@ -6,11 +6,15 @@ import { useRef } from "react"; import { Button } from "@/components/ui/button"; import { FacadePattern } from "@/components/ui/facade-pattern"; import { FadeIn, SlideIn } from "@/components/ui/motion/motion-components"; +import { useSpotlight } from "@/hooks/use-spotlight"; import { ConfigPreview } from "./config-preview"; export default function EternalCombatPage() { const targetRef = useRef(null); + const previewSpotlight = useSpotlight(); + const featureSpotlight = useSpotlight(); + const configSpotlight = useSpotlight(); const { scrollYProgress } = useScroll({ target: targetRef, offset: ["start start", "end start"], @@ -129,7 +133,11 @@ export default function EternalCombatPage() { {/* Code Preview Overlay */}
-
+
@@ -211,7 +219,11 @@ export default function EternalCombatPage() { }, ].map((feature, i) => ( -
+
@@ -261,7 +273,11 @@ export default function EternalCombatPage() {
{/* The "Long Screenshot" Container */} -
+
{/* Tilted Content */}
{/* Inner blur container */} diff --git a/app/projects/eternalcore/eternal-showcase.tsx b/app/projects/eternalcore/eternal-showcase.tsx index ba7f57cc..9a829ac8 100644 --- a/app/projects/eternalcore/eternal-showcase.tsx +++ b/app/projects/eternalcore/eternal-showcase.tsx @@ -6,6 +6,7 @@ import Image from "next/image"; import { useEffect, useState } from "react"; import { createPortal } from "react-dom"; import { FadeIn } from "@/components/ui/motion/motion-components"; +import { useSpotlight } from "@/hooks/use-spotlight"; const marqueeItems = [ // Row 1: Essentials & Chat @@ -124,6 +125,8 @@ const InfiniteMarquee = ({ speed?: number; onItemClick: (item: (typeof marqueeItems)[0]) => void; }) => { + const spotlight = useSpotlight(); + return (
{[...items, ...items].map((item, i) => ( onItemClick(item)} + onPointerLeave={spotlight.onPointerLeave} + onPointerMove={spotlight.onPointerMove} whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} > diff --git a/app/projects/eternalcore/page.tsx b/app/projects/eternalcore/page.tsx index fef6b8fd..50136be8 100644 --- a/app/projects/eternalcore/page.tsx +++ b/app/projects/eternalcore/page.tsx @@ -8,6 +8,7 @@ import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { FacadePattern } from "@/components/ui/facade-pattern"; import { FadeIn, MotionSection, ScaleIn, SlideIn } from "@/components/ui/motion/motion-components"; +import { useSpotlight } from "@/hooks/use-spotlight"; import { slideUp } from "@/lib/animations/variants"; import { ConfigPreview } from "./config-preview"; @@ -15,6 +16,8 @@ import { EternalShowcase } from "./eternal-showcase"; export default function EternalCorePage() { const targetRef = useRef(null); + const bannerSpotlight = useSpotlight(); + const previewSpotlight = useSpotlight(); const { scrollYProgress } = useScroll({ target: targetRef, offset: ["start start", "end start"], @@ -97,7 +100,11 @@ export default function EternalCorePage() { {/* Project Banner Placeholder */}
-
+
EternalCore Project Banner
{/* The "Long Screenshot" Container */} -
+
{/* Window Controls */}
diff --git a/components/builds/build-table.tsx b/components/builds/build-table.tsx index 0a969e49..edf98aca 100644 --- a/components/builds/build-table.tsx +++ b/components/builds/build-table.tsx @@ -1,6 +1,9 @@ +"use client"; + import { AnimatePresence } from "framer-motion"; import { Loader2, Package } from "lucide-react"; import type { Project } from "@/app/api/builds/builds"; +import { useSpotlight } from "@/hooks/use-spotlight"; import { type Build, BuildRow } from "./build-row"; interface BuildTableProps { @@ -18,6 +21,8 @@ export function BuildTable({ lastDownloadedId, onDownload, }: BuildTableProps) { + const spotlight = useSpotlight(); + return (
{loading ? ( @@ -29,7 +34,11 @@ export function BuildTable({

Fetching builds for {project.name}…

) : ( -
+
diff --git a/components/cookie-consent-modal.tsx b/components/cookie-consent-modal.tsx index 93e8a636..cb7312c0 100644 --- a/components/cookie-consent-modal.tsx +++ b/components/cookie-consent-modal.tsx @@ -8,6 +8,7 @@ import { Button } from "@/components/ui/button"; import { Switch } from "@/components/ui/switch"; import { useCookieConsent } from "@/hooks/use-cookie-consent"; import { useReducedMotion } from "@/hooks/use-reduced-motion"; +import { useSpotlight } from "@/hooks/use-spotlight"; import { interactionSpring } from "@/lib/animations/variants"; // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Component logic is complex by design @@ -16,6 +17,7 @@ export function CookieConsentModal() { const [isOpen, setIsOpen] = useState(false); const [showDetails, setShowDetails] = useState(false); const prefersReducedMotion = useReducedMotion(); + const spotlight = useSpotlight(); const isDefaultConsent = consent.necessary && !consent.analytics && !consent.marketing && !consent.preferences; @@ -84,7 +86,7 @@ export function CookieConsentModal() { {isOpen ? (
diff --git a/components/docs/content/docs-navigation.tsx b/components/docs/content/docs-navigation.tsx index ef68cabd..e4cf5fbc 100644 --- a/components/docs/content/docs-navigation.tsx +++ b/components/docs/content/docs-navigation.tsx @@ -4,6 +4,7 @@ import { motion } from "framer-motion"; import { ArrowLeft, ArrowRight } from "lucide-react"; import Link from "next/link"; import { useReducedMotion } from "@/hooks/use-reduced-motion"; +import { useSpotlight } from "@/hooks/use-spotlight"; import { cn } from "@/lib/utils"; interface NavigationLink { @@ -18,6 +19,8 @@ interface DocsNavigationProps { export function DocsNavigation({ prev, next }: DocsNavigationProps) { const prefersReducedMotion = useReducedMotion(); + const prevSpotlight = useSpotlight(); + const nextSpotlight = useSpotlight(); return (
diff --git a/components/docs/eternalcore/placeholder/placeholder-table.tsx b/components/docs/eternalcore/placeholder/placeholder-table.tsx index 1ae385af..46335767 100644 --- a/components/docs/eternalcore/placeholder/placeholder-table.tsx +++ b/components/docs/eternalcore/placeholder/placeholder-table.tsx @@ -4,6 +4,7 @@ import { AnimatePresence, motion } from "framer-motion"; import type { Placeholder } from "@/components/docs/eternalcore/placeholder/types"; import { CopyToClipboard } from "@/components/ui/copy-to-clipboard"; +import { useSpotlight } from "@/hooks/use-spotlight"; import { cn } from "@/lib/utils"; interface PlaceholderTableProps { @@ -11,8 +12,14 @@ interface PlaceholderTableProps { } export function PlaceholderTable({ placeholders }: PlaceholderTableProps) { + const spotlight = useSpotlight(); + return ( -
+
diff --git a/components/docs/search/search-modal.tsx b/components/docs/search/search-modal.tsx index c4d8370f..21e941da 100644 --- a/components/docs/search/search-modal.tsx +++ b/components/docs/search/search-modal.tsx @@ -13,6 +13,7 @@ import { useClickOutside } from "@/hooks/use-click-outside"; import { useRecentSearches } from "@/hooks/use-recent-searches"; import { useReducedMotion } from "@/hooks/use-reduced-motion"; import { useSearch } from "@/hooks/use-search"; +import { useSpotlight } from "@/hooks/use-spotlight"; import { cn } from "@/lib/utils"; interface SearchModalProps { @@ -53,6 +54,7 @@ export function SearchModal({ isOpen, onClose, triggerRef }: SearchModalProps) { const modalRef = useRef(null); const inputRef = useRef(null); + const spotlight = useSpotlight(); const router = useRouter(); const prefersReducedMotion = useReducedMotion(); @@ -265,7 +267,11 @@ export function SearchModal({ isOpen, onClose, triggerRef }: SearchModalProps) { }} > {/* Modal Container with Enhanced Glassmorphism */} -
+
{/* Search Input Section with Enhanced Gradient */}
diff --git a/components/docs/sidebar/netlify-highlight.tsx b/components/docs/sidebar/netlify-highlight.tsx index e70fcd34..2d4ad130 100644 --- a/components/docs/sidebar/netlify-highlight.tsx +++ b/components/docs/sidebar/netlify-highlight.tsx @@ -1,6 +1,11 @@ +"use client"; + import { SiNetlify } from "react-icons/si"; +import { useSpotlight } from "@/hooks/use-spotlight"; export function NetlifyHighlight() { + const spotlight = useSpotlight(); + return ( -
+
diff --git a/components/docs/sidebar/sidebar-wrapper.tsx b/components/docs/sidebar/sidebar-wrapper.tsx index da4a11f1..cc794ac5 100644 --- a/components/docs/sidebar/sidebar-wrapper.tsx +++ b/components/docs/sidebar/sidebar-wrapper.tsx @@ -5,6 +5,7 @@ import type { FC } from "react"; import { useEffect, useRef, useState } from "react"; import { SearchModal } from "@/components/docs/search/search-modal"; import { SearchTrigger } from "@/components/docs/search/search-trigger"; +import { useSpotlight } from "@/hooks/use-spotlight"; import DocSidebar from "./doc-sidebar"; import type { DocItem } from "./types"; @@ -16,6 +17,7 @@ const SidebarWrapper: FC = ({ sidebarStructure }) => { const sidebarRef = useRef(null); const lenisRef = useRef(null); const [isSearchOpen, setIsSearchOpen] = useState(false); + const spotlight = useSpotlight(); useEffect(() => { const sidebarElement = sidebarRef.current; @@ -64,7 +66,9 @@ const SidebarWrapper: FC = ({ sidebarStructure }) => {
setIsSearchOpen(true)} />
diff --git a/components/hero/terminal/terminal-window.tsx b/components/hero/terminal/terminal-window.tsx index 431d6a30..81402abc 100644 --- a/components/hero/terminal/terminal-window.tsx +++ b/components/hero/terminal/terminal-window.tsx @@ -1,6 +1,7 @@ "use client"; import { type ChangeEvent, type KeyboardEvent, useEffect, useRef, useState } from "react"; +import { useSpotlight } from "@/hooks/use-spotlight"; import responses from "./responses"; @@ -21,6 +22,7 @@ export default function Terminal() { const [input, setInput] = useState(""); const inputRef = useRef(null); const scrollRef = useRef(null); + const spotlight = useSpotlight(); useEffect(() => { scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight, behavior: "smooth" }); @@ -58,7 +60,11 @@ export default function Terminal() { }; return ( -
+
{/* Title bar */}
diff --git a/components/home/about/about-section.tsx b/components/home/about/about-section.tsx index ba18e4e8..5ea1d8ae 100644 --- a/components/home/about/about-section.tsx +++ b/components/home/about/about-section.tsx @@ -7,9 +7,12 @@ import PolandMap from "@/components/home/about/poland-map"; import PeopleGroupIcon from "@/components/icons/people-group"; import { Button } from "@/components/ui/button"; import { SlideIn } from "@/components/ui/motion/motion-components"; +import { useSpotlight } from "@/hooks/use-spotlight"; import AboutImage from "@/public/hero image.png"; export default function About() { + const spotlight = useSpotlight(); + return (
diff --git a/components/home/projects/projects-section.tsx b/components/home/projects/projects-section.tsx index 8bd95c25..36ab200b 100644 --- a/components/home/projects/projects-section.tsx +++ b/components/home/projects/projects-section.tsx @@ -4,6 +4,7 @@ import { motion } from "framer-motion"; import Image from "next/image"; import Link from "next/link"; import { useEffect, useRef, useState } from "react"; +import { useSpotlight } from "@/hooks/use-spotlight"; interface Project { name: string; @@ -54,6 +55,7 @@ const projects: Project[] = [ export default function Projects() { const scrollRef = useRef(null); const [isHovered, setIsHovered] = useState(false); + const containerSpotlight = useSpotlight(); // Auto-scroll logic useEffect(() => { @@ -122,9 +124,11 @@ export default function Projects() { {/* biome-ignore lint/a11y/noStaticElementInteractions: Pause on hover feature */} {/* biome-ignore lint/a11y/noNoninteractiveElementInteractions: Pause on hover feature */}
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} + onPointerLeave={containerSpotlight.onPointerLeave} + onPointerMove={containerSpotlight.onPointerMove} >
(); + return ( ("chat"); const [errors, setErrors] = useState>({}); const soundTabRef = useRef(null); + const spotlight = useSpotlight(); const handleChange = useCallback( (field: FieldType, value: string | boolean) => { @@ -108,7 +110,11 @@ export function NotificationGenerator({ notification, setNotification }: Notific }, [activeTab]); return ( -
+
& { ref?: React.Ref }) => ( -
-); +}: React.HTMLAttributes & { ref?: React.Ref }) => { + const spotlight = useSpotlight(); + + const handlePointerMove: React.PointerEventHandler = (event) => { + spotlight.onPointerMove(event); + props.onPointerMove?.(event); + }; + + const handlePointerLeave: React.PointerEventHandler = (event) => { + spotlight.onPointerLeave(event); + props.onPointerLeave?.(event); + }; + + return ( +
+ ); +}; Card.displayName = "Card"; const CardHeader = ({ diff --git a/components/ui/mdx/card.tsx b/components/ui/mdx/card.tsx index c8169a25..950d8253 100644 --- a/components/ui/mdx/card.tsx +++ b/components/ui/mdx/card.tsx @@ -6,6 +6,7 @@ import * as LucideIcons from "lucide-react"; import Link from "next/link"; import type { ReactNode } from "react"; import { cn } from "@/lib/utils"; +import { useSpotlight } from "@/hooks/use-spotlight"; interface CardProps { title: string; @@ -18,16 +19,19 @@ interface CardProps { export function Card({ title, description, icon, href, className }: CardProps) { // biome-ignore lint/performance/noDynamicNamespaceImportAccess: Optimized by Next.js optimizePackageImports const IconComponent = icon ? (LucideIcons[icon as keyof typeof LucideIcons] as LucideIcon) : null; + const spotlight = useSpotlight(); const content = (
{!!IconComponent && ( diff --git a/components/ui/mdx/link-preview.tsx b/components/ui/mdx/link-preview.tsx index d47e73fe..2d4c3003 100644 --- a/components/ui/mdx/link-preview.tsx +++ b/components/ui/mdx/link-preview.tsx @@ -4,6 +4,7 @@ import { ExternalLink } from "lucide-react"; import Link from "next/link"; import type { ReactNode } from "react"; import { cn } from "@/lib/utils"; +import { useSpotlight } from "@/hooks/use-spotlight"; interface LinkPreviewProps { href: string; @@ -15,13 +16,16 @@ interface LinkPreviewProps { export function LinkPreview({ href, title, description, icon, className }: LinkPreviewProps) { const isExternal = href.startsWith("http://") || href.startsWith("https://"); + const spotlight = useSpotlight(); const content = (
{icon && (
diff --git a/hooks/use-spotlight.ts b/hooks/use-spotlight.ts new file mode 100644 index 00000000..cf112e70 --- /dev/null +++ b/hooks/use-spotlight.ts @@ -0,0 +1,34 @@ +"use client"; + +import type { PointerEvent as ReactPointerEvent } from "react"; +import { useCallback, useRef } from "react"; + +export function useSpotlight() { + const frameRef = useRef(null); + + const onPointerMove = useCallback((event: ReactPointerEvent) => { + const { clientX, clientY, currentTarget } = event; + + if (frameRef.current !== null) { + cancelAnimationFrame(frameRef.current); + } + + frameRef.current = requestAnimationFrame(() => { + const rect = currentTarget.getBoundingClientRect(); + const x = clientX - rect.left; + const y = clientY - rect.top; + + currentTarget.style.setProperty("--spotlight-x", `${x}px`); + currentTarget.style.setProperty("--spotlight-y", `${y}px`); + }); + }, []); + + const onPointerLeave = useCallback((event: ReactPointerEvent) => { + const { currentTarget } = event; + + currentTarget.style.setProperty("--spotlight-x", "50%"); + currentTarget.style.setProperty("--spotlight-y", "50%"); + }, []); + + return { onPointerMove, onPointerLeave }; +}