diff --git a/app/globals.css b/app/globals.css
index 7f59790..0279b52 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -41,6 +41,14 @@
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
+ --color-main: var(--main);
+ --color-main-secondary: var(--main-secondary);
+ --color-main-foreground: var(--main-foreground);
+ --color-main-muted: var(--main-muted);
+ --color-main-background: var(--main-background);
+ --color-main-invert: var(--main-invert);
+ --color-primary-muted: var(--primary-muted);
+ --color-primary-invert: var(--primary-invert);
}
:root {
@@ -51,7 +59,7 @@
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
- --primary: oklch(0.205 0 0);
+ --primary: oklch(0 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
@@ -76,6 +84,16 @@
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
+
+ /* Luxe UI variables */
+ --main: oklch(0.97 0 0);
+ --main-secondary: oklch(97% 0 0);
+ --main-foreground: oklch(0.925 0 0);
+ --main-muted: oklch(0.96 0 0);
+ --main-background: oklch(0.97 0 0);
+ --main-invert: oklch(0.205 0 0);
+ --primary-muted: oklch(0.556 0 0);
+ --primary-invert: oklch(1 0 0);
}
.dark {
@@ -85,7 +103,7 @@
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
- --primary: oklch(0.922 0 0);
+ --primary: oklch(1 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
@@ -110,17 +128,57 @@
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
+
+ /* Luxe UI dark mode variables */
+ --main: oklch(0.178 0 0);
+ --main-foreground: oklch(0.269 0 0);
+ --main-muted: oklch(0.168 0 0);
+ --main-background: oklch(0.145 0 0);
+ --primary-muted: oklch(0.708 0 0);
+ --primary-invert: oklch(0 0 0);
}
+
+
@layer base {
* {
@apply border-border outline-ring/50;
}
+ html {
+ scroll-behavior: smooth;
+ }
html,
body,
#__next {
- @apply h-full overflow-hidden;
+ @apply h-full;
+ }
+
+ body {
+ @apply overflow-x-hidden overflow-y-auto;
+ }
+
+ /* shadcn-inspired scrollbar */
+ :root,
+ html {
+ scrollbar-width: thin;
+ scrollbar-color: var(--muted-foreground) transparent;
+ }
+ ::-webkit-scrollbar {
+ width: 10px;
+ height: 10px;
+ }
+ ::-webkit-scrollbar-track {
+ background: transparent;
+ }
+ ::-webkit-scrollbar-thumb {
+ background-color: color-mix(in oklab, var(--muted-foreground) 45%, transparent);
+ border-radius: 9999px;
+ border: 2px solid transparent;
+ background-clip: padding-box;
+ }
+ ::-webkit-scrollbar-thumb:hover {
+ background-color: color-mix(in oklab, var(--muted-foreground) 65%, transparent);
}
body {
@@ -141,6 +199,43 @@
.animate-breathe {
animation: breathe 5s ease-in-out infinite;
}
+
+ @keyframes shine {
+ 0% {
+ background-position: 0% 0%;
+ }
+ 50% {
+ background-position: 100% 100%;
+ }
+ 100% {
+ background-position: 0% 0%;
+ }
+ }
+
+ .animate-shine {
+ animation: shine 10s linear infinite;
+ }
+
+ @keyframes brightness {
+ 0% {
+ opacity: 0;
+ transform: translateX(-100%) skewX(-45deg);
+ }
+ 5% {
+ opacity: 1;
+ }
+ 95% {
+ opacity: 1;
+ }
+ 100% {
+ opacity: 0;
+ transform: translateX(200%) skewX(-45deg);
+ }
+ }
+
+ .animate-brightness {
+ animation: brightness 2s ease-in-out infinite;
+ }
}
/* Tiptap Editor Styles */
diff --git a/app/page.tsx b/app/page.tsx
index ca4726b..75dfa92 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,11 +1,23 @@
'use client'
-import { Button } from "@/components/ui/button";
import Link from "next/link";
+import Image from "next/image";
import { AsciiEye } from "@/components/ascii-eye";
-import Dither from '@/components/dither'
import { useState, useEffect } from 'react';
import TextType from "@/components/text-type"
+import DecryptedText from "@/components/fancy/text/decrypted-text";
+import { LuxeButton } from "@/components/ui/luxe-button";
+import {
+ NavigationMenu,
+ NavigationMenuList,
+ NavigationMenuItem,
+ NavigationMenuLink,
+} from "@/components/ui/navigation-menu";
+import Dither from "@/components/dither";
+import { BrowserComponent } from "@/components/browser-component";
+import { FeaturesAlternating } from "@/components/features-alternating";
+import { ModelsSection } from "@/components/models-section";
+import { SiteFooter } from "@/components/site-footer";
export default function Home() {
const [isLoading, setIsLoading] = useState(true);
@@ -28,6 +40,113 @@ export default function Home() {
loadContent();
}, []);
+ const featuresContent = [
+ {
+ title: "Live Dashboard",
+ description: "Dashboard showcasing all current RTMP streams with automated video asset creation.",
+ content: (
+
+
+
+
+
+ ),
+ },
+ {
+ title: "Entity & Event Detection",
+ description: "Entity and event detection with AI-powered analysis.",
+ content: (
+
+
+
+
+
+ ),
+ },
+ {
+ title: "Analytics & Statistics",
+ description: "Comprehensive stats from all events and anomalies detected.",
+ content: (
+
+
+
+
+
+ ),
+ },
+ {
+ title: "Automated Reports",
+ description: "Auto-generated editable rich incident reports based on detected events.",
+ content: (
+
+
+
+
+
+ ),
+ },
+ {
+ title: "Elasticsearch Agent",
+ description: "Search through all detected anomalies and events with powerful Elasticsearch Agent integration.",
+ content: (
+
+
+
+
+
+ ),
+ },
+ {
+ title: "AI Assistant",
+ description: "Search for specific event types, ask questions, generate reports, and more.",
+ content: (
+
+
+
+
+
+ ),
+ },
+ ];
+
return (
<>
{/* Loading Screen */}
@@ -37,52 +156,92 @@ export default function Home() {
fadeOut ? 'opacity-0' : 'opacity-100'
}`}
>
-
+
+
+
+
)}
- {/* Main Content */}
-
- {/* Dither background */}
-
-
-
+
+
+
+
+
+
+
+
+ Overview
+
+
+ Models
+
+
+ Features
+
+
+ Dashboard
+
+
+
+
- {/* Left side - Main content */}
-
-
-
ARGUS
-
+
+
+
+
+
ARGUS
+
+ Open Argus
+
- {/* Right side - ASCII Eye */}
-
-
+
+
+
+
+
Technology for the Future
+
Argus is built on the latest and greatest technologies to deliver the best possible experience for you and your team.
+
+
+
+
+
+
+
Features
+
+ Discover the powerful capabilities that make Argus the ultimate surveillance and monitoring solution.
+
+
+
+
+
+
>
);
}
\ No newline at end of file
diff --git a/components.json b/components.json
index b7b9791..9c87867 100644
--- a/components.json
+++ b/components.json
@@ -18,5 +18,8 @@
"lib": "@/lib",
"hooks": "@/hooks"
},
- "registries": {}
+ "registries": {
+ "@aceternity": "https://ui.aceternity.com/registry/{name}.json",
+ "@fancy": "https://fancycomponents.dev/r/{name}.json"
+ }
}
diff --git a/components/ascii-eye.tsx b/components/ascii-eye.tsx
index f8bea7c..4e4457e 100644
--- a/components/ascii-eye.tsx
+++ b/components/ascii-eye.tsx
@@ -71,9 +71,6 @@ interface CharData {
export const AsciiEye = memo(function AsciiEye() {
const spansRef = useRef
([]);
const nonWhitespaceIndicesRef = useRef([]);
- const rafIdRef = useRef(undefined);
- const lastColorUpdateRef = useRef(0);
- const lastScrambleUpdateRef = useRef(0);
const scrambleTimeoutRef = useRef(undefined);
// Memoize character data - only parse once
@@ -122,77 +119,64 @@ export const AsciiEye = memo(function AsciiEye() {
return data;
}, []);
- // Optimized animation loop using requestAnimationFrame
+ // Use setInterval instead of RAF for much better performance
useEffect(() => {
- let isActive = true;
+ let colorInterval: NodeJS.Timeout | undefined;
+ let scrambleInterval: NodeJS.Timeout | undefined;
- const animate = (timestamp: number) => {
- if (!isActive) return;
-
- // Color update - every 800ms, but only update a batch of characters
- if (timestamp - lastColorUpdateRef.current >= 800) {
- lastColorUpdateRef.current = timestamp;
-
- // Batch update colors - only update a subset for better performance
- const batchSize = Math.ceil(nonWhitespaceIndicesRef.current.length / 3);
- const startIdx = Math.floor(Math.random() * (nonWhitespaceIndicesRef.current.length - batchSize));
+ // Color update - every 2000ms (increased from 800ms)
+ colorInterval = setInterval(() => {
+ // Update only a small batch of characters
+ const batchSize = Math.min(50, Math.ceil(nonWhitespaceIndicesRef.current.length / 10));
+ const startIdx = Math.floor(Math.random() * (nonWhitespaceIndicesRef.current.length - batchSize));
+
+ for (let i = startIdx; i < startIdx + batchSize && i < nonWhitespaceIndicesRef.current.length; i++) {
+ const idx = nonWhitespaceIndicesRef.current[i];
+ const char = charData[idx];
+ const span = spansRef.current[idx];
- for (let i = startIdx; i < startIdx + batchSize && i < nonWhitespaceIndicesRef.current.length; i++) {
- const idx = nonWhitespaceIndicesRef.current[i];
- const char = charData[idx];
- const span = spansRef.current[idx];
-
- if (span) {
- const newColor = getRandomColor(char.isDarkChar);
- span.style.color = newColor;
- }
+ if (span) {
+ const newColor = getRandomColor(char.isDarkChar);
+ span.style.color = newColor;
}
}
+ }, 2000);
- // Character scrambling - every 1000ms
- if (timestamp - lastScrambleUpdateRef.current >= 1000) {
- lastScrambleUpdateRef.current = timestamp;
-
- const numToScramble = 15 + Math.floor(Math.random() * 5);
- const scrambledIndices: number[] = [];
+ // Character scrambling - every 2000ms (increased from 1000ms)
+ scrambleInterval = setInterval(() => {
+ const numToScramble = 10; // Reduced from 15-20
+ const scrambledIndices: number[] = [];
+
+ // Scramble random characters
+ for (let i = 0; i < numToScramble; i++) {
+ const randomIdx = Math.floor(Math.random() * nonWhitespaceIndicesRef.current.length);
+ const idx = nonWhitespaceIndicesRef.current[randomIdx];
+ scrambledIndices.push(idx);
- // Scramble random characters
- for (let i = 0; i < numToScramble; i++) {
- const randomIdx = Math.floor(Math.random() * nonWhitespaceIndicesRef.current.length);
- const idx = nonWhitespaceIndicesRef.current[randomIdx];
- scrambledIndices.push(idx);
-
- const span = spansRef.current[idx];
- if (span) {
- span.textContent = getRandomBrailleChar();
- }
- }
-
- // Reset scrambled characters after delay
- if (scrambleTimeoutRef.current) {
- window.clearTimeout(scrambleTimeoutRef.current);
+ const span = spansRef.current[idx];
+ if (span) {
+ span.textContent = getRandomBrailleChar();
}
-
- scrambleTimeoutRef.current = window.setTimeout(() => {
- scrambledIndices.forEach((idx) => {
- const span = spansRef.current[idx];
- if (span) {
- span.textContent = charData[idx].originalChar;
- }
- });
- }, 150);
}
- rafIdRef.current = requestAnimationFrame(animate);
- };
-
- rafIdRef.current = requestAnimationFrame(animate);
+ // Reset scrambled characters after delay
+ if (scrambleTimeoutRef.current) {
+ window.clearTimeout(scrambleTimeoutRef.current);
+ }
+
+ scrambleTimeoutRef.current = window.setTimeout(() => {
+ scrambledIndices.forEach((idx) => {
+ const span = spansRef.current[idx];
+ if (span) {
+ span.textContent = charData[idx].originalChar;
+ }
+ });
+ }, 150);
+ }, 2000);
return () => {
- isActive = false;
- if (rafIdRef.current) {
- cancelAnimationFrame(rafIdRef.current);
- }
+ if (colorInterval) clearInterval(colorInterval);
+ if (scrambleInterval) clearInterval(scrambleInterval);
if (scrambleTimeoutRef.current) {
window.clearTimeout(scrambleTimeoutRef.current);
}
diff --git a/components/browser-component.tsx b/components/browser-component.tsx
new file mode 100644
index 0000000..aca3a04
--- /dev/null
+++ b/components/browser-component.tsx
@@ -0,0 +1,51 @@
+import { cn } from '@/utils/cn'
+
+export const BrowserComponent: React.FC<{ children?: React.ReactNode; className?: string; url?: string }> = ({
+ className,
+ children,
+ url = "yourwebsite.com/admin/sales"
+}) => (
+
+)
diff --git a/components/fancy/text/decrypted-text.tsx b/components/fancy/text/decrypted-text.tsx
new file mode 100644
index 0000000..5b0d664
--- /dev/null
+++ b/components/fancy/text/decrypted-text.tsx
@@ -0,0 +1,83 @@
+"use client";
+
+import { useEffect, useMemo, useRef, useState } from "react";
+
+type DecryptedTextProps = {
+ text: string;
+ revealDurationMs?: number;
+ scrambleSpeed?: number;
+ characters?: string;
+ className?: string;
+};
+
+export default function DecryptedText({
+ text,
+ revealDurationMs = 1500,
+ scrambleSpeed = 30,
+ characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+{}[]<>?",
+ className,
+}: DecryptedTextProps) {
+ const [display, setDisplay] = useState(text);
+ const startTsRef = useRef(null);
+ const rafRef = useRef(null);
+ const intervalRef = useRef | null>(null);
+
+ const charPool = useMemo(() => characters.split(""), [characters]);
+
+ useEffect(() => {
+ // Reset state when text changes
+ setDisplay(text);
+ startTsRef.current = null;
+
+ const tick = (ts: number) => {
+ if (startTsRef.current == null) startTsRef.current = ts;
+ const elapsed = ts - startTsRef.current;
+ const progress = Math.min(1, elapsed / revealDurationMs);
+ const revealCount = Math.floor(progress * text.length);
+
+ setDisplay((prev) => {
+ const revealed = text.slice(0, revealCount);
+ const remaining = text
+ .slice(revealCount)
+ .split("")
+ .map((ch) => (ch === " " ? " " : charPool[Math.floor(Math.random() * charPool.length)]))
+ .join("");
+ return revealed + remaining;
+ });
+
+ if (progress < 1) {
+ rafRef.current = requestAnimationFrame(tick);
+ } else {
+ setDisplay(text);
+ }
+ };
+
+ // Keep scrambling unrevealed characters between RAF frames for a smoother effect
+ intervalRef.current = setInterval(() => {
+ if (startTsRef.current == null) return;
+ const elapsed = performance.now() - startTsRef.current;
+ const progress = Math.min(1, elapsed / revealDurationMs);
+ const revealCount = Math.floor(progress * text.length);
+ setDisplay((prev) => {
+ const revealed = text.slice(0, revealCount);
+ const remaining = text
+ .slice(revealCount)
+ .split("")
+ .map((ch) => (ch === " " ? " " : charPool[Math.floor(Math.random() * charPool.length)]))
+ .join("");
+ return revealed + remaining;
+ });
+ }, scrambleSpeed);
+
+ rafRef.current = requestAnimationFrame(tick);
+
+ return () => {
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
+ if (intervalRef.current) clearInterval(intervalRef.current);
+ };
+ }, [text, revealDurationMs, scrambleSpeed, charPool]);
+
+ return {display};
+}
+
+
diff --git a/components/fancy/text/scramble-hover.tsx b/components/fancy/text/scramble-hover.tsx
new file mode 100644
index 0000000..6ae8025
--- /dev/null
+++ b/components/fancy/text/scramble-hover.tsx
@@ -0,0 +1,188 @@
+"use client"
+
+import { useEffect, useState } from "react"
+import { motion } from "motion/react"
+
+import { cn } from "@/lib/utils"
+
+interface ScrambleHoverProps {
+ text: string
+ scrambleSpeed?: number
+ maxIterations?: number
+ sequential?: boolean
+ revealDirection?: "start" | "end" | "center"
+ useOriginalCharsOnly?: boolean
+ characters?: string
+ className?: string
+ scrambledClassName?: string
+}
+
+const ScrambleHover: React.FC = ({
+ text,
+ scrambleSpeed = 50,
+ maxIterations = 10,
+ useOriginalCharsOnly = false,
+ characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&*()_+",
+ className,
+ scrambledClassName,
+ sequential = false,
+ revealDirection = "start",
+ ...props
+}) => {
+ const [displayText, setDisplayText] = useState(text)
+ const [isHovering, setIsHovering] = useState(false)
+ const [isScrambling, setIsScrambling] = useState(false)
+ const [revealedIndices, setRevealedIndices] = useState(new Set())
+
+ useEffect(() => {
+ let interval: NodeJS.Timeout
+ let currentIteration = 0
+
+ const getNextIndex = () => {
+ const textLength = text.length
+ switch (revealDirection) {
+ case "start":
+ return revealedIndices.size
+ case "end":
+ return textLength - 1 - revealedIndices.size
+ case "center":
+ const middle = Math.floor(textLength / 2)
+ const offset = Math.floor(revealedIndices.size / 2)
+ const nextIndex =
+ revealedIndices.size % 2 === 0
+ ? middle + offset
+ : middle - offset - 1
+
+ if (
+ nextIndex >= 0 &&
+ nextIndex < textLength &&
+ !revealedIndices.has(nextIndex)
+ ) {
+ return nextIndex
+ }
+
+ for (let i = 0; i < textLength; i++) {
+ if (!revealedIndices.has(i)) return i
+ }
+ return 0
+ default:
+ return revealedIndices.size
+ }
+ }
+
+ const shuffleText = (text: string) => {
+ if (useOriginalCharsOnly) {
+ const positions = text.split("").map((char, i) => ({
+ char,
+ isSpace: char === " ",
+ index: i,
+ isRevealed: revealedIndices.has(i),
+ }))
+
+ const nonSpaceChars = positions
+ .filter((p) => !p.isSpace && !p.isRevealed)
+ .map((p) => p.char)
+
+ // Shuffle remaining non-revealed, non-space characters
+ for (let i = nonSpaceChars.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1))
+ ;[nonSpaceChars[i], nonSpaceChars[j]] = [
+ nonSpaceChars[j],
+ nonSpaceChars[i],
+ ]
+ }
+
+ let charIndex = 0
+ return positions
+ .map((p) => {
+ if (p.isSpace) return " "
+ if (p.isRevealed) return text[p.index]
+ return nonSpaceChars[charIndex++]
+ })
+ .join("")
+ } else {
+ return text
+ .split("")
+ .map((char, i) => {
+ if (char === " ") return " "
+ if (revealedIndices.has(i)) return text[i]
+ return availableChars[
+ Math.floor(Math.random() * availableChars.length)
+ ]
+ })
+ .join("")
+ }
+ }
+
+ const availableChars = useOriginalCharsOnly
+ ? Array.from(new Set(text.split(""))).filter((char) => char !== " ")
+ : characters.split("")
+
+ if (isHovering) {
+ setIsScrambling(true)
+ interval = setInterval(() => {
+ if (sequential) {
+ if (revealedIndices.size < text.length) {
+ const nextIndex = getNextIndex()
+ revealedIndices.add(nextIndex)
+ setDisplayText(shuffleText(text))
+ } else {
+ clearInterval(interval)
+ setIsScrambling(false)
+ }
+ } else {
+ setDisplayText(shuffleText(text))
+ currentIteration++
+ if (currentIteration >= maxIterations) {
+ clearInterval(interval)
+ setIsScrambling(false)
+ setDisplayText(text)
+ }
+ }
+ }, scrambleSpeed)
+ } else {
+ setDisplayText(text)
+ revealedIndices.clear()
+ }
+
+ return () => {
+ if (interval) clearInterval(interval)
+ }
+ }, [
+ isHovering,
+ text,
+ characters,
+ scrambleSpeed,
+ useOriginalCharsOnly,
+ sequential,
+ revealDirection,
+ maxIterations,
+ ])
+
+ return (
+ setIsHovering(true)}
+ onHoverEnd={() => setIsHovering(false)}
+ className={cn("inline-block whitespace-pre-wrap", className)}
+ {...props}
+ >
+ {displayText}
+
+ {displayText.split("").map((char, index) => (
+
+ {char}
+
+ ))}
+
+
+ )
+}
+
+export default ScrambleHover
diff --git a/components/fancy/text/text-highlighter.tsx b/components/fancy/text/text-highlighter.tsx
new file mode 100644
index 0000000..0e7d035
--- /dev/null
+++ b/components/fancy/text/text-highlighter.tsx
@@ -0,0 +1,211 @@
+"use client"
+
+import {
+ ElementType,
+ forwardRef,
+ useEffect,
+ useImperativeHandle,
+ useMemo,
+ useRef,
+ useState,
+} from "react"
+import { motion, Transition, useInView, UseInViewOptions } from "motion/react"
+
+import { cn } from "@/lib/utils"
+
+type HighlightDirection = "ltr" | "rtl" | "ttb" | "btt"
+
+type TextHighlighterProps = {
+ /**
+ * The text content to be highlighted
+ */
+ children: React.ReactNode
+
+ /**
+ * HTML element to render as
+ * @default "p"
+ */
+ as?: ElementType
+
+ /**
+ * How to trigger the animation
+ * @default "inView"
+ */
+ triggerType?: "hover" | "ref" | "inView" | "auto"
+
+ /**
+ * Animation transition configuration
+ * @default { duration: 0.4, type: "spring", bounce: 0 }
+ */
+ transition?: Transition
+
+ /**
+ * Options for useInView hook when triggerType is "inView"
+ */
+ useInViewOptions?: UseInViewOptions
+
+ /**
+ * Class name for the container element
+ */
+ className?: string
+
+ /**
+ * Highlight color (CSS color string). Also can be a function that returns a color string, eg:
+ * @default 'hsl(60, 90%, 68%)' (yellow)
+ */
+ highlightColor?: string
+
+ /**
+ * Direction of the highlight animation
+ * @default "ltr" (left to right)
+ */
+ direction?: HighlightDirection
+} & React.HTMLAttributes
+
+export type TextHighlighterRef = {
+ /**
+ * Trigger the highlight animation
+ * @param direction - Optional direction override for this animation
+ */
+ animate: (direction?: HighlightDirection) => void
+
+ /**
+ * Reset the highlight animation
+ */
+ reset: () => void
+}
+
+export const TextHighlighter = forwardRef<
+ TextHighlighterRef,
+ TextHighlighterProps
+>(
+ (
+ {
+ children,
+ as = "span",
+ triggerType = "inView",
+ transition = { type: "spring", duration: 1, delay: 0, bounce: 0 },
+ useInViewOptions = {
+ once: true,
+ initial: false,
+ amount: 0.1,
+ },
+ className,
+ highlightColor = "hsl(25, 90%, 80%)",
+ direction = "ltr",
+ ...props
+ },
+ ref
+ ) => {
+ const componentRef = useRef(null)
+ const [isAnimating, setIsAnimating] = useState(false)
+ const [isHovered, setIsHovered] = useState(false)
+ const [currentDirection, setCurrentDirection] =
+ useState(direction)
+
+ // this allows us to change the direction whenever the direction prop changes
+ useEffect(() => {
+ setCurrentDirection(direction)
+ }, [direction])
+
+ const isInView =
+ triggerType === "inView"
+ ? useInView(componentRef, useInViewOptions)
+ : false
+
+ useImperativeHandle(ref, () => ({
+ animate: (animationDirection?: HighlightDirection) => {
+ if (animationDirection) {
+ setCurrentDirection(animationDirection)
+ }
+ setIsAnimating(true)
+ },
+ reset: () => setIsAnimating(false),
+ }))
+
+ const shouldAnimate =
+ triggerType === "hover"
+ ? isHovered
+ : triggerType === "inView"
+ ? isInView
+ : triggerType === "ref"
+ ? isAnimating
+ : triggerType === "auto"
+ ? true
+ : false
+
+ const ElementTag = as || "span"
+
+ // Get background size based on direction
+ const getBackgroundSize = (animated: boolean) => {
+ switch (currentDirection) {
+ case "ltr":
+ return animated ? "100% 100%" : "0% 100%"
+ case "rtl":
+ return animated ? "100% 100%" : "0% 100%"
+ case "ttb":
+ return animated ? "100% 100%" : "100% 0%"
+ case "btt":
+ return animated ? "100% 100%" : "100% 0%"
+ default:
+ return animated ? "100% 100%" : "0% 100%"
+ }
+ }
+
+ // Get background position based on direction
+ const getBackgroundPosition = () => {
+ switch (currentDirection) {
+ case "ltr":
+ return "0% 0%"
+ case "rtl":
+ return "100% 0%"
+ case "ttb":
+ return "0% 0%"
+ case "btt":
+ return "0% 100%"
+ default:
+ return "0% 0%"
+ }
+ }
+
+ const animatedSize = useMemo(() => getBackgroundSize(shouldAnimate), [shouldAnimate, currentDirection])
+ const initialSize = useMemo(() => getBackgroundSize(false), [currentDirection])
+ const backgroundPosition = useMemo(() => getBackgroundPosition(), [currentDirection])
+
+ const highlightStyle = {
+ backgroundImage: `linear-gradient(${highlightColor}, ${highlightColor})`,
+ backgroundRepeat: "no-repeat",
+ backgroundPosition: backgroundPosition,
+ backgroundSize: animatedSize,
+ boxDecorationBreak: "clone",
+ WebkitBoxDecorationBreak: "clone",
+ } as React.CSSProperties
+
+ return (
+ triggerType === "hover" && setIsHovered(true)}
+ onMouseLeave={() => triggerType === "hover" && setIsHovered(false)}
+ {...props}
+ >
+
+ {children}
+
+
+ )
+ }
+)
+
+TextHighlighter.displayName = "TextHighlighter"
+
+export default TextHighlighter
diff --git a/components/features-alternating.tsx b/components/features-alternating.tsx
new file mode 100644
index 0000000..d9d000e
--- /dev/null
+++ b/components/features-alternating.tsx
@@ -0,0 +1,141 @@
+"use client";
+
+import { useEffect, useRef } from "react";
+import { gsap } from "gsap";
+import { ScrollTrigger } from "gsap/ScrollTrigger";
+import { LuxeCard, LuxeCardHeader, LuxeCardTitle, LuxeCardDescription } from "@/components/ui/luxe-card";
+
+gsap.registerPlugin(ScrollTrigger);
+
+interface FeaturesAlternatingProps {
+ features: Array<{
+ title: string;
+ description: string;
+ content: React.ReactNode;
+ }>;
+}
+
+export function FeaturesAlternating({
+ features,
+}: FeaturesAlternatingProps) {
+ const sectionRefs = useRef<(HTMLDivElement | null)[]>([]);
+
+ useEffect(() => {
+ sectionRefs.current.forEach((section, index) => {
+ if (!section) return;
+
+ const isEven = index % 2 === 0;
+ const cardElement = section.querySelector('.feature-card');
+ const contentElement = section.querySelector('.feature-content');
+
+ // Animate card sliding in from its side
+ if (cardElement) {
+ gsap.fromTo(
+ cardElement,
+ {
+ x: isEven ? -100 : 100,
+ opacity: 0,
+ },
+ {
+ x: 0,
+ opacity: 1,
+ duration: 0.8,
+ ease: "power3.out",
+ scrollTrigger: {
+ trigger: section,
+ start: "top 80%",
+ end: "top 50%",
+ toggleActions: "play none none reverse",
+ },
+ }
+ );
+ }
+
+ // Animate content sliding in from opposite side
+ if (contentElement) {
+ gsap.fromTo(
+ contentElement,
+ {
+ x: isEven ? 100 : -100,
+ opacity: 0,
+ },
+ {
+ x: 0,
+ opacity: 1,
+ duration: 0.8,
+ ease: "power3.out",
+ scrollTrigger: {
+ trigger: section,
+ start: "top 80%",
+ end: "top 50%",
+ toggleActions: "play none none reverse",
+ },
+ }
+ );
+ }
+ });
+
+ return () => {
+ ScrollTrigger.getAll().forEach((trigger) => trigger.kill());
+ };
+ }, [features]);
+
+ return (
+
+ {features.map((feature, index) => {
+ const isEven = index % 2 === 0;
+
+ return (
+
{
+ sectionRefs.current[index] = el;
+ }}
+ className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-start"
+ >
+ {isEven ? (
+ <>
+ {/* Left: Text Content */}
+
+
+
+ {feature.title}
+
+
+ {feature.description}
+
+
+
+
+ {/* Right: Browser Component */}
+
+ {feature.content}
+
+ >
+ ) : (
+ <>
+ {/* Left: Browser Component */}
+
+ {feature.content}
+
+
+ {/* Right: Text Content */}
+
+
+
+ {feature.title}
+
+
+ {feature.description}
+
+
+
+ >
+ )}
+
+ );
+ })}
+
+ );
+}
+
diff --git a/components/landing-animated-tabs.tsx b/components/landing-animated-tabs.tsx
new file mode 100644
index 0000000..f9894ec
--- /dev/null
+++ b/components/landing-animated-tabs.tsx
@@ -0,0 +1,76 @@
+"use client"; // @NOTE: Add in case you are using Next.js
+
+import { useEffect, useRef } from "react";
+
+import { cn } from "@/utils/cn";
+
+type AnimatedTabsProps = {
+ tabs: Array;
+ activeTab: string;
+ onTabChange: (tab: string) => void;
+};
+
+export function AnimatedTabs({ tabs, activeTab, onTabChange }: AnimatedTabsProps) {
+
+ const containerRef = useRef(null);
+ const activeTabRef = useRef(null);
+
+ useEffect(() => {
+ const container = containerRef.current;
+
+ if (container && activeTab) {
+ const activeTabElement = activeTabRef.current;
+
+ if (activeTabElement) {
+ const { offsetLeft, offsetWidth } = activeTabElement;
+
+ const clipLeft = offsetLeft;
+ const clipRight = offsetLeft + offsetWidth;
+
+ container.style.clipPath = `inset(0 ${Number(100 - (clipRight / container.offsetWidth) * 100).toFixed()}% 0 ${Number((clipLeft / container.offsetWidth) * 100).toFixed()}% round 17px)`;
+ }
+ }
+ }, [activeTab]);
+
+ return (
+
+
+
+ {tabs.map((tab, index) => (
+
+ ))}
+
+
+
+ {tabs.map((tab, index) => {
+ const isActive = activeTab === tab;
+
+ return (
+
+ );
+ })}
+
+
+ );
+}
diff --git a/components/models-section.tsx b/components/models-section.tsx
new file mode 100644
index 0000000..925c576
--- /dev/null
+++ b/components/models-section.tsx
@@ -0,0 +1,188 @@
+"use client";
+
+import TextHighlighter from "@/components/fancy/text/text-highlighter";
+import ScrambleHover from "@/components/fancy/text/scramble-hover";
+import { Badge } from "@/components/ui/badge";
+
+const aiModels = [
+ "Letta Stateful Agent",
+ "Elasticsearch Agent",
+ "Groq Kimi K2 Instruct",
+ "Gemini 2.5 Pro",
+ "Claude 4.5 Haiku",
+ "Claude 4.5 Sonnet",
+ "Roboflow 3.0 Object Detection",
+];
+
+export function ModelsSection() {
+ return (
+
+
+ {/* Left: Text with Highlighter */}
+
+
+ Argus leverages the{" "}
+
+ best in AI technology
+
+ , combining cutting-edge{" "}
+
+ computer vision
+ {" "}
+ and{" "}
+
+ natural language processing
+ {" "}
+ to deliver unparalleled{" "}
+
+ security and surveillance
+ {" "}
+ capabilities. Argus intelligently analyzes video feeds in{" "}
+
+ real-time
+ , detecting {" "}
+
+ anomalies
+ , identifying threats, and generating{" "}
+
+ comprehensive reports
+ to keep you informed and protected.
+
+
+
+
+
+ Built on a {" "}
+
+ real-time streaming pipeline
+ {" "}
+ with {" "}
+
+ Mux
+ {" "}
+ and {" "}
+
+ FFmpeg transmuxing
+
+ , Argus delivers {" "}
+
+ low-latency playback
+ {" "}
+ and resilient delivery while segments are scheduled and analyzed at the edge.
+
+
+
+ Detections from {" "}
+
+ Roboflow object detection
+ {" "}
+ and AI summaries from {" "}
+
+ Gemini
+ {" "}
+ are indexed into {" "}
+
+ Elasticsearch
+ {" "}
+ with rich {" "}
+
+ structured metadata
+
+ , and persisted via {" "}
+
+ Supabase Realtime & Edge Functions
+ {" "}
+ so you're always in the loop.
+
+
+
+ {/* Right: AI Models List */}
+
+
+
+ Powered by
+
+
+
+ {aiModels.map((model, index) => (
+
+
+
+ ))}
+
+
+
+
+ );
+}
+
diff --git a/components/site-footer.tsx b/components/site-footer.tsx
new file mode 100644
index 0000000..a4fb290
--- /dev/null
+++ b/components/site-footer.tsx
@@ -0,0 +1,33 @@
+"use client";
+
+export function SiteFooter() {
+ return (
+
+ );
+}
+
+
diff --git a/components/ui/flip-words.tsx b/components/ui/flip-words.tsx
new file mode 100644
index 0000000..43e1cc5
--- /dev/null
+++ b/components/ui/flip-words.tsx
@@ -0,0 +1,98 @@
+"use client";
+import React, { useCallback, useEffect, useRef, useState } from "react";
+import { AnimatePresence, motion, LayoutGroup } from "motion/react";
+import { cn } from "@/lib/utils";
+
+export const FlipWords = ({
+ words,
+ duration = 3000,
+ className,
+}: {
+ words: string[];
+ duration?: number;
+ className?: string;
+}) => {
+ const [currentWord, setCurrentWord] = useState(words[0]);
+ const [isAnimating, setIsAnimating] = useState(false);
+
+ // thanks for the fix Julian - https://github.com/Julian-AT
+ const startAnimation = useCallback(() => {
+ const word = words[words.indexOf(currentWord) + 1] || words[0];
+ setCurrentWord(word);
+ setIsAnimating(true);
+ }, [currentWord, words]);
+
+ useEffect(() => {
+ if (!isAnimating)
+ setTimeout(() => {
+ startAnimation();
+ }, duration);
+ }, [isAnimating, duration, startAnimation]);
+
+ return (
+ {
+ setIsAnimating(false);
+ }}
+ >
+
+ {/* edit suggested by Sajal: https://x.com/DewanganSajal */}
+ {currentWord.split(" ").map((word, wordIndex) => (
+
+ {word.split("").map((letter, letterIndex) => (
+
+ {letter}
+
+ ))}
+
+
+ ))}
+
+
+ );
+};
diff --git a/components/ui/luxe-button.tsx b/components/ui/luxe-button.tsx
new file mode 100644
index 0000000..baad29a
--- /dev/null
+++ b/components/ui/luxe-button.tsx
@@ -0,0 +1,271 @@
+'use client' // @NOTE: Add in case you are using Next.js
+
+import { useRef, useState } from 'react'
+
+import { motion } from 'motion/react'
+import * as Slot from '@radix-ui/react-slot'
+
+import { cn } from '@/utils/cn'
+
+type Variant = {
+ variant: string
+ component: React.FC>
+}
+
+const variants = [
+ {
+ variant: 'default',
+ component: ({ className, ...props }) => (
+
+ ),
+ },
+ {
+ variant: 'outline',
+ component: ({ className, ...props }) => (
+
+ ),
+ },
+ {
+ variant: 'success',
+ component: ({ className, ...props }) => (
+
+ ),
+ },
+ {
+ variant: 'destructive',
+ component: ({ className, ...props }) => (
+
+ ),
+ },
+ {
+ variant: 'shine',
+ component: ({ className, ...props }) => (
+
+ ),
+ },
+ {
+ variant: 'animated-border',
+ component: ({ children, className, ...props }) => (
+
+ ),
+ },
+ {
+ variant: 'rotate-border',
+ component: ({ children, className, ...props }) => (
+
+ ),
+ },
+ {
+ variant: 'glitch-brightness',
+ component: ({ children, className, ...props }) => {
+ return (
+
+ )
+
+ function TextGlitch({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ {children}
+
+
+ {children}
+
+
+ )
+ }
+
+ function Brightness() {
+ return (
+
+ )
+ }
+ },
+ },
+] as const satisfies readonly Variant[]
+
+export type LuxeButtonProps = {
+ variant?: (typeof variants)[number]['variant']
+ isMagnetic?: boolean
+} & React.ComponentProps<'button'>
+
+export function LuxeButton({
+ variant = 'default',
+ isMagnetic = false,
+ className,
+ ...props
+}: LuxeButtonProps) {
+ const FALLBACK_INDEX = 0
+
+ const variantComponent = variants.find(v => v.variant === variant)?.component
+
+ const Component = variantComponent || variants[FALLBACK_INDEX].component
+
+ const buttonContent = (
+
+
+
+ )
+
+ if (isMagnetic) {
+ return (
+
+ {buttonContent}
+
+ )
+ }
+
+ return buttonContent
+}
+
+function Magnetic({ children }: { children: React.ReactNode }) {
+ const { ref, handleMouseMove, handleMouseLeave, x, y } = useMagnetic()
+
+ return (
+
+ {children}
+
+ )
+}
+
+function useMagnetic() {
+ const [position, setPosition] = useState({ x: 0, y: 0 })
+ const ref = useRef(null)
+
+ function handleMouseMove(e: React.MouseEvent) {
+ const { clientX, clientY } = e
+ const { height, width, left, top } = ref.current!.getBoundingClientRect()
+
+ const middleX = clientX - (left + width / 2)
+ const middleY = clientY - (top + height / 2)
+
+ setPosition({ x: middleX, y: middleY })
+ }
+
+ function handleMouseLeave() {
+ setPosition({ x: 0, y: 0 })
+ }
+
+ const { x, y } = position
+
+ return { ref, handleMouseMove, handleMouseLeave, x, y }
+}
diff --git a/components/ui/navigation-menu.tsx b/components/ui/navigation-menu.tsx
new file mode 100644
index 0000000..3b50737
--- /dev/null
+++ b/components/ui/navigation-menu.tsx
@@ -0,0 +1,210 @@
+'use client' // @NOTE: Add in case you are using Next.js
+
+import { useState } from 'react'
+
+import * as RadixNavigationMenu from '@radix-ui/react-navigation-menu'
+import { AnimatePresence, motion, useReducedMotion } from 'motion/react'
+
+import { cn } from '@/utils/cn'
+
+type NavigationMenuProps = React.ComponentProps
+
+export function NavigationMenu({
+ className,
+ children,
+ ...props
+}: NavigationMenuProps) {
+ return (
+
+ {children}
+
+ )
+}
+
+type NavigationMenuListProps = React.ComponentProps<
+ typeof RadixNavigationMenu.List
+>
+
+export function NavigationMenuList({
+ className,
+ ...props
+}: NavigationMenuListProps) {
+ return (
+
+ )
+}
+
+type NavigationMenuItemProps = React.ComponentProps<
+ typeof RadixNavigationMenu.Item
+>
+
+export function NavigationMenuItem({
+ className,
+ ...props
+}: NavigationMenuItemProps) {
+ return (
+
+ )
+}
+
+type NavigationMenuTriggerProps = React.ComponentProps<
+ typeof RadixNavigationMenu.Trigger
+>
+
+export function NavigationMenuTrigger({
+ className,
+ children,
+ ...props
+}: NavigationMenuTriggerProps) {
+ return (
+
+ {children}
+
+
+ )
+}
+
+type NavigationMenuContentProps = React.ComponentProps<
+ typeof RadixNavigationMenu.Content
+>
+
+export function NavigationMenuContent({
+ className,
+ ...props
+}: NavigationMenuContentProps) {
+ return (
+
+ )
+}
+
+type NavigationMenuContentItemProps = React.ComponentProps<
+ typeof RadixNavigationMenu.Link
+>
+
+export function NavigationMenuContentItem({
+ children,
+ className,
+ ...props
+}: NavigationMenuContentItemProps) {
+ const [isHovered, setIsHovered] = useState(false)
+
+ const isReducedMotion = useReducedMotion()
+
+ const interactionProps = !isReducedMotion && {
+ onMouseEnter: () => setIsHovered(true),
+ onMouseLeave: () => setIsHovered(false),
+ onFocus: () => setIsHovered(true),
+ onBlur: () => setIsHovered(false),
+ }
+
+ return (
+
+
+ {children}
+
+ {!isReducedMotion && (
+
+ {isHovered && (
+
+ )}
+
+ )}
+
+ )
+}
+
+type NavigationMenuLinkProps = React.ComponentProps<
+ typeof RadixNavigationMenu.Link
+>
+
+export function NavigationMenuLink({
+ className,
+ ...props
+}: NavigationMenuLinkProps) {
+ return (
+
+ )
+}
diff --git a/components/ui/sticky-scroll-reveal.tsx b/components/ui/sticky-scroll-reveal.tsx
new file mode 100644
index 0000000..1d99094
--- /dev/null
+++ b/components/ui/sticky-scroll-reveal.tsx
@@ -0,0 +1,97 @@
+"use client";
+import React, { useEffect, useRef, useState } from "react";
+import { useMotionValueEvent, useScroll } from "motion/react";
+import { motion } from "motion/react";
+import { cn } from "@/lib/utils";
+
+export const StickyScroll = ({
+ content,
+ contentClassName,
+ onActiveCardChange,
+ scrollContainerRef,
+}: {
+ content: {
+ title: string;
+ description: string;
+ content?: React.ReactNode | any;
+ }[];
+ contentClassName?: string;
+ onActiveCardChange?: (index: number) => void;
+ scrollContainerRef?: React.RefObject;
+}) => {
+ const [activeCard, setActiveCard] = React.useState(0);
+ const ref = scrollContainerRef || useRef(null);
+ const { scrollYProgress } = useScroll({
+ // uncomment line 22 and comment line 23 if you DONT want the overflow container and want to have it change on the entire page scroll
+ // target: ref
+ container: ref,
+ offset: ["start start", "end start"],
+ });
+ const cardLength = content.length;
+
+ useMotionValueEvent(scrollYProgress, "change", (latest) => {
+ // Calculate which card should be active based on scroll progress
+ // Divide scroll progress into equal segments for each card
+ const newActiveCard = Math.min(
+ cardLength - 1,
+ Math.floor(latest * cardLength)
+ );
+
+ if (newActiveCard !== activeCard) {
+ setActiveCard(newActiveCard);
+ if (onActiveCardChange) {
+ onActiveCardChange(newActiveCard);
+ }
+ }
+ });
+
+
+ return (
+
+
+
+ {content.map((item, index) => (
+
+
+
+ {item.title}
+
+
+ {item.description}
+
+
+
+ ))}
+
+
+
+
+ {content[activeCard].content ?? null}
+
+
+ );
+};
diff --git a/package.json b/package.json
index 9068e16..bda5e9f 100644
--- a/package.json
+++ b/package.json
@@ -45,6 +45,7 @@
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-label": "^2.1.7",
+ "@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-scroll-area": "^1.2.10",
@@ -101,6 +102,7 @@
"gsap": "^3.13.0",
"lodash.throttle": "^4.1.1",
"lucide-react": "^0.546.0",
+ "mini-svg-data-uri": "^1.4.4",
"marked": "^16.4.1",
"motion": "^12.23.24",
"nanoid": "^5.1.6",
@@ -118,6 +120,7 @@
"sonner": "^2.0.7",
"streamdown": "^1.4.0",
"tailwind-merge": "^3.3.1",
+ "tailwind-variants": "^3.1.1",
"three": "^0.180.0",
"tokenlens": "^1.3.1",
"use-stick-to-bottom": "^1.1.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 018e2a0..f224917 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -77,6 +77,9 @@ importers:
'@radix-ui/react-label':
specifier: ^2.1.7
version: 2.1.7(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-navigation-menu':
+ specifier: ^1.2.14
+ version: 1.2.14(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-popover':
specifier: ^1.1.15
version: 1.1.15(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -248,6 +251,9 @@ importers:
marked:
specifier: ^16.4.1
version: 16.4.1
+ mini-svg-data-uri:
+ specifier: ^1.4.4
+ version: 1.4.4
motion:
specifier: ^12.23.24
version: 12.23.24(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -296,6 +302,9 @@ importers:
tailwind-merge:
specifier: ^3.3.1
version: 3.3.1
+ tailwind-variants:
+ specifier: ^3.1.1
+ version: 3.1.1(tailwind-merge@3.3.1)(tailwindcss@4.1.15)
three:
specifier: ^0.180.0
version: 0.180.0
@@ -1131,6 +1140,19 @@ packages:
'@types/react-dom':
optional: true
+ '@radix-ui/react-navigation-menu@1.2.14':
+ resolution: {integrity: sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
'@radix-ui/react-popover@1.1.15':
resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==}
peerDependencies:
@@ -3186,6 +3208,10 @@ packages:
min-document@2.19.0:
resolution: {integrity: sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==}
+ mini-svg-data-uri@1.4.4:
+ resolution: {integrity: sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==}
+ hasBin: true
+
minimatch@3.1.2:
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
@@ -3742,6 +3768,16 @@ packages:
tailwind-merge@3.3.1:
resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==}
+ tailwind-variants@3.1.1:
+ resolution: {integrity: sha512-ftLXe3krnqkMHsuBTEmaVUXYovXtPyTK7ckEfDRXS8PBZx0bAUas+A0jYxuKA5b8qg++wvQ3d2MQ7l/xeZxbZQ==}
+ engines: {node: '>=16.x', pnpm: '>=7.x'}
+ peerDependencies:
+ tailwind-merge: '>=3.0.0'
+ tailwindcss: '*'
+ peerDependenciesMeta:
+ tailwind-merge:
+ optional: true
+
tailwindcss@4.1.15:
resolution: {integrity: sha512-k2WLnWkYFkdpRv+Oby3EBXIyQC8/s1HOFMBUViwtAh6Z5uAozeUSMQlIsn/c6Q2iJzqG6aJT3wdPaRNj70iYxQ==}
@@ -4762,6 +4798,28 @@ snapshots:
'@types/react': 19.2.2
'@types/react-dom': 19.2.2(@types/react@19.2.2)
+ '@radix-ui/react-navigation-menu@1.2.14(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.3
+ '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.1.0)
+ '@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.1.0)
+ '@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.1.0)
+ '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.1.0)
+ '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.1.0)
+ '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.1.0)
+ '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.1.0)
+ '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.2)(react@19.1.0)
+ '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+ react: 19.1.0
+ react-dom: 19.1.0(react@19.1.0)
+ optionalDependencies:
+ '@types/react': 19.2.2
+ '@types/react-dom': 19.2.2(@types/react@19.2.2)
+
'@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/primitive': 1.1.3
@@ -7163,6 +7221,8 @@ snapshots:
dependencies:
dom-walk: 0.1.2
+ mini-svg-data-uri@1.4.4: {}
+
minimatch@3.1.2:
dependencies:
brace-expansion: 1.1.12
@@ -7868,6 +7928,12 @@ snapshots:
tailwind-merge@3.3.1: {}
+ tailwind-variants@3.1.1(tailwind-merge@3.3.1)(tailwindcss@4.1.15):
+ dependencies:
+ tailwindcss: 4.1.15
+ optionalDependencies:
+ tailwind-merge: 3.3.1
+
tailwindcss@4.1.15: {}
tapable@2.3.0: {}
diff --git a/public/assets/arguschat.webp b/public/assets/arguschat.webp
new file mode 100644
index 0000000..6625a30
Binary files /dev/null and b/public/assets/arguschat.webp differ
diff --git a/public/assets/argusdash.webp b/public/assets/argusdash.webp
new file mode 100644
index 0000000..79b6de7
Binary files /dev/null and b/public/assets/argusdash.webp differ
diff --git a/public/assets/argusdetection.webp b/public/assets/argusdetection.webp
new file mode 100644
index 0000000..3a83670
Binary files /dev/null and b/public/assets/argusdetection.webp differ
diff --git a/public/assets/argusreports.webp b/public/assets/argusreports.webp
new file mode 100644
index 0000000..4c561dd
Binary files /dev/null and b/public/assets/argusreports.webp differ
diff --git a/public/assets/argussearch.webp b/public/assets/argussearch.webp
new file mode 100644
index 0000000..ffd9e00
Binary files /dev/null and b/public/assets/argussearch.webp differ
diff --git a/public/assets/argusstats.webp b/public/assets/argusstats.webp
new file mode 100644
index 0000000..8adf46c
Binary files /dev/null and b/public/assets/argusstats.webp differ
diff --git a/utils/cn.ts b/utils/cn.ts
new file mode 100644
index 0000000..365058c
--- /dev/null
+++ b/utils/cn.ts
@@ -0,0 +1,6 @@
+import { type ClassValue, clsx } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}