+
{post.tags.map((tag) => (
-
+
{tag.name}
))}
-
-
-
-
-
- Posted {formatDate(post.createdAt)}
-
-
-
+
+
+
+ {formatDate(post.createdAt)}
+
+
+
+ {readingTime} min read
+
+
);
}
diff --git a/app/forum/layout.tsx b/app/forum/layout.tsx
index 92d7320..90e41bd 100644
--- a/app/forum/layout.tsx
+++ b/app/forum/layout.tsx
@@ -1,18 +1,9 @@
-import {
- HomeIcon,
- InfoIcon,
- MessageCirclePlusIcon,
- SquareCodeIcon,
-} from "lucide-react";
import type { Metadata } from "next";
-import Link from "next/link";
import { redirect } from "next/navigation";
-
import { logout } from "@/actions/auth";
-import { Button } from "@/components/ui/button";
+import { DockNav } from "@/components/dock-nav";
import { getSession } from "@/lib/auth";
import { ForumNavbar } from "./components/forum-navbar";
-import { LogoutButton } from "./components/logout-button";
export const metadata: Metadata = {
title: "Umedu — Private Forum",
@@ -37,26 +28,7 @@ export default async function ForumLayout({
{children}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
);
}
diff --git a/app/forum/page.tsx b/app/forum/page.tsx
index 5f2f91d..648eced 100644
--- a/app/forum/page.tsx
+++ b/app/forum/page.tsx
@@ -3,10 +3,23 @@
import { useThrottledCallback } from "@tanstack/react-pacer/throttler";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useWindowVirtualizer } from "@tanstack/react-virtual";
-import { AlertCircleIcon, MessageCircleDashedIcon } from "lucide-react";
+import {
+ AlertCircleIcon,
+ MessageCircleDashedIcon,
+ MessageCirclePlusIcon,
+ ShieldCheckIcon,
+} from "lucide-react";
+import Link from "next/link";
import { useEffect } from "react";
+import DecryptedText from "@/components/DecryptedText";
import { HoverPrefetchLink } from "@/components/hover-prefetch-link";
+import ShinyText from "@/components/ShinyText";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { Card, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
+import { ProgressiveBlur } from "@/components/ui/progressive-blur";
+import { Separator } from "@/components/ui/separator";
import type { Post, Tag } from "@/db/schema";
import { PostCard } from "./components/post-card";
import { PostCardSkeleton } from "./components/post-card-skeleton";
@@ -108,63 +121,124 @@ export default function FeedPage() {
return (
+
+
+
+
+
+
+
+
+
+ usls.edu.ph
+
+
+ Your campus peers are reading in real time. Share honest wins,
+ worries, or random shower thoughts.
+
+
+
+
+
+
+
+
+ Share it anonymously in seconds.
+
+
+
+
+
+
+
+
+
+
+
+ Craft a post
+
+
+
+
+
+
+
{allPosts.length === 0 && !isFetching && (
- No posts yet
-
- Start the conversation by creating a new post!
+ No posts just yet
+
+
+ Your note could be the first spark - drop a question or story.
+
+
+ Start the conversation
+
)}
-
- {items.map((virtualRow) => {
- const isLoaderRow = virtualRow.index > allPosts.length - 1;
- const post = allPosts[virtualRow.index];
-
- if (!isLoaderRow && !post) return null;
-
- return (
-
- {isLoaderRow ? (
- hasNextPage ? (
-
+
+
+
+
+ Latest discussions
+
+
+ {items.map((virtualRow) => {
+ const isLoaderRow = virtualRow.index > allPosts.length - 1;
+ const post = allPosts[virtualRow.index];
+
+ if (!isLoaderRow && !post) return null;
+
+ return (
+
+ {isLoaderRow ? (
+ hasNextPage ? (
+
+ ) : (
+
+ Nothing more to load
+
+ )
) : (
-
- Nothing more to load
-
- )
- ) : (
-
-
-
- )}
-
- );
- })}
-
+
+
+
+ )}
+
+ );
+ })}
+
+
);
}
diff --git a/app/forum/submit/page.tsx b/app/forum/submit/page.tsx
index 5aa4f5f..4c7e9c3 100644
--- a/app/forum/submit/page.tsx
+++ b/app/forum/submit/page.tsx
@@ -1,11 +1,27 @@
"use client";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
-import { SendHorizonalIcon, XIcon } from "lucide-react";
+import {
+ ArrowLeftIcon,
+ LightbulbIcon,
+ SendHorizonalIcon,
+ ShieldCheckIcon,
+ SparklesIcon,
+ XIcon,
+} from "lucide-react";
+import Link from "next/link";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
import { z } from "zod/v4";
import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { useAppForm } from "@/hooks/form";
import { getTagsQuery } from "@/lib/queries";
@@ -28,6 +44,36 @@ export default function SubmitPage() {
const queryClient = useQueryClient();
const { data: tags } = useQuery(getTagsQuery);
+ const postingTips = [
+ {
+ title: "Lead with context",
+ description:
+ "Share just enough detail so peers can quickly understand the situation.",
+ icon: LightbulbIcon,
+ },
+ {
+ title: "Ask one clear question",
+ description:
+ "Let people know how they can help - advice, resources, or a vibe check.",
+ icon: SparklesIcon,
+ },
+ ];
+
+ const safetyChecklist = [
+ {
+ title: "Protect identities",
+ description:
+ "Skip names, classes, or any info that could point to a specific person.",
+ icon: ShieldCheckIcon,
+ },
+ {
+ title: "Keep it kind",
+ description:
+ "We moderate for empathy and respect - critique ideas, not people.",
+ icon: SparklesIcon,
+ },
+ ];
+
const mutation = useMutation({
mutationFn: (values: z.infer
) => {
return fetch("/api/posts", {
@@ -68,95 +114,209 @@ export default function SubmitPage() {
});
return (
- (
-
+
+
+
+
+
+
+ Submit anonymously
+
+
+ Give your story the care it deserves.
+
+
+ A thoughtful title and a little context go a long way. Your post is
+ only visible to other students, and your identity stays hidden.
+
+
+
+
+
+
(
+
+ )}
/>
- )}
- />
-
- {
- const selectedTags = field.state.value;
-
- return (
-
-
Tags
-
-
- {tags && selectedTags.length > 0 ? (
- selectedTags.map((tagId) => {
- const tag = tags.find((t) => t.id === tagId);
- if (!tag) return null;
-
- return (
-
- {tag.name}
-
- field.handleChange(
- selectedTags.filter((id) => id !== tagId),
- )
- }
- disabled={mutation.isPending}
- >
-
-
-
- );
- })
- ) : (
-
- No tags selected
-
- )}
-
-
-
- );
- }}
- />
-
- (
- {
+ const selectedTags = field.state.value;
+
+ return (
+
+
+ Tags
+ {selectedTags.length > 0 && (
+ field.handleChange([])}
+ className="text-xs font-medium uppercase tracking-wide text-muted-foreground hover:text-foreground"
+ disabled={mutation.isPending}
+ >
+ Clear all
+
+ )}
+
+
+ Pick up to 3 tags so the right people find your post faster.
+
+
+
+ {tags && selectedTags.length > 0 ? (
+ selectedTags.map((tagId) => {
+ const tag = tags.find((t) => t.id === tagId);
+ if (!tag) return null;
+
+ return (
+
+ {tag.name}
+
+ field.handleChange(
+ selectedTags.filter((id) => id !== tagId),
+ )
+ }
+ disabled={mutation.isPending}
+ aria-label={`Remove ${tag.name} tag`}
+ className="inline-flex size-5 items-center justify-center rounded-full bg-black/10 text-muted-foreground transition hover:bg-black/20 disabled:opacity-40 dark:bg-white/10 dark:hover:bg-white/20"
+ >
+
+
+
+ );
+ })
+ ) : (
+
+ No tags yet - add a few for quicker discovery.
+
+ )}
+
+
+ {!tags && (
+
+ Fetching available tags...
+
+ )}
+
+
+
+ );
+ }}
/>
- )}
- />
-
-
-
- (
+
+ )}
/>
-
+
+
+
+ router.push("/forum")}
+ disabled={mutation.isPending}
+ >
+ Cancel
+
+
+
+
+
+
+
+
+
+ Posting tips
+
+ These prompts keep responses helpful and quick.
+
+
+
+
+ {postingTips.map(({ title, description, icon: Icon }) => (
+
+
+
+
+
+
{title}
+
+ {description}
+
+
+
+ ))}
+
+
+
+
+
+
+ Care checklist
+
+ A quick reminder before you hit submit.
+
+
+
+
+ {safetyChecklist.map(({ title, description, icon: Icon }) => (
+
+
+
+
+
+
{title}
+
+ {description}
+
+
+
+ ))}
+
+
+
+
-
+
);
}
diff --git a/app/globals.css b/app/globals.css
index 181792e..f0ae184 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -11,15 +11,15 @@
}
:root {
- --radius: 0.65rem;
+ --radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.141 0.005 285.823);
- --primary: oklch(0.795 0.184 86.047);
- --primary-foreground: oklch(0.421 0.095 57.708);
+ --primary: oklch(0.21 0.006 285.885);
+ --primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.967 0.001 286.375);
@@ -29,7 +29,7 @@
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
- --ring: oklch(0.795 0.184 86.047);
+ --ring: oklch(0.705 0.015 286.067);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
@@ -37,12 +37,12 @@
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.141 0.005 285.823);
- --sidebar-primary: oklch(0.795 0.184 86.047);
- --sidebar-primary-foreground: oklch(0.421 0.095 57.708);
+ --sidebar-primary: oklch(0.21 0.006 285.885);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.967 0.001 286.375);
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32);
- --sidebar-ring: oklch(0.795 0.184 86.047);
+ --sidebar-ring: oklch(0.705 0.015 286.067);
}
.dark {
@@ -52,8 +52,8 @@
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.21 0.006 285.885);
--popover-foreground: oklch(0.985 0 0);
- --primary: oklch(0.795 0.184 86.047);
- --primary-foreground: oklch(0.421 0.095 57.708);
+ --primary: oklch(0.92 0.004 286.32);
+ --primary-foreground: oklch(0.21 0.006 285.885);
--secondary: oklch(0.274 0.006 286.033);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.274 0.006 286.033);
@@ -63,7 +63,7 @@
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
- --ring: oklch(0.554 0.135 66.442);
+ --ring: oklch(0.552 0.016 285.938);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
@@ -71,12 +71,12 @@
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.21 0.006 285.885);
--sidebar-foreground: oklch(0.985 0 0);
- --sidebar-primary: oklch(0.795 0.184 86.047);
- --sidebar-primary-foreground: oklch(0.421 0.095 57.708);
+ --sidebar-primary: oklch(0.488 0.243 264.376);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.274 0.006 286.033);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
- --sidebar-ring: oklch(0.554 0.135 66.442);
+ --sidebar-ring: oklch(0.552 0.016 285.938);
}
@theme inline {
@@ -135,4 +135,32 @@
[role="button"]:not(:disabled) {
cursor: pointer;
}
+
+ .shiny-text {
+ display: inline-block;
+ background-image: linear-gradient(
+ 120deg,
+ rgba(255, 255, 255, 0) 40%,
+ rgba(255, 255, 255, 0.8) 50%,
+ rgba(255, 255, 255, 0) 60%
+ );
+ background-size: 200% 100%;
+ -webkit-background-clip: text;
+ background-clip: text;
+ -webkit-text-fill-color: transparent; /* ensure text shows the background gradient */
+ color: transparent;
+ }
+
+ .animate-shine {
+ animation: shine var(--shine-duration, 5s) linear infinite;
+ }
+
+ @keyframes shine {
+ 0% {
+ background-position: 100%;
+ }
+ 100% {
+ background-position: -100%;
+ }
+ }
}
diff --git a/app/page.tsx b/app/page.tsx
index a6d0e4d..b61a44c 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,13 +1,30 @@
import { MessageSquareTextIcon } from "lucide-react";
import Link from "next/link";
+import DecryptedText from "@/components/DecryptedText";
+import { FlatMap } from "@/components/flat-map";
import { Footer } from "@/components/footer";
import { HighlightText } from "@/components/highlight-text";
+import LightRays from "@/components/LightRays";
import { Navbar } from "@/components/navbar";
+import ShinyText from "@/components/ShinyText";
import { Button } from "@/components/ui/button";
export default function Home() {
return (
-
+
+
@@ -15,12 +32,19 @@ export default function Home() {
um
edu
- open-source, anonymous, encrypted, private edu forums
+ open-source, anonymous,
+
+ , private edu forums
- Access your private forum
+
+
@@ -49,13 +73,22 @@ export default function Home() {
email. No personal information is stored, ensuring your privacy is
protected.{" "}
-
- Learn more →
+
+ Learn more{" "}
+
+ {" "}
+ →
+
-
+
+
+
);
diff --git a/app/posts/[id]/components/post-date.tsx b/app/posts/[id]/components/post-date.tsx
index 0c70db0..f31112d 100644
--- a/app/posts/[id]/components/post-date.tsx
+++ b/app/posts/[id]/components/post-date.tsx
@@ -1,11 +1,13 @@
"use client";
+import { CalendarIcon } from "lucide-react";
import { formatDate } from "@/lib/utils";
export function PostDate({ createdAt }: { createdAt: Date }) {
return (
-
- Posted at {formatDate(createdAt)}
-
+
+
+ {formatDate(createdAt)}
+
);
}
diff --git a/app/posts/[id]/page.tsx b/app/posts/[id]/page.tsx
index 59fc77c..d023e8b 100644
--- a/app/posts/[id]/page.tsx
+++ b/app/posts/[id]/page.tsx
@@ -1,12 +1,21 @@
import { eq } from "drizzle-orm";
+import { ArrowLeftIcon } from "lucide-react";
import type { Metadata } from "next";
import { unstable_cache } from "next/cache";
+import Link from "next/link";
import { notFound } from "next/navigation";
import Markdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { ForumNavbar } from "@/app/forum/components/forum-navbar";
import { Footer } from "@/components/footer";
import { Badge } from "@/components/ui/badge";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import { db } from "@/db";
import { postTable } from "@/db/schema";
@@ -55,33 +64,62 @@ export default async function Page({ params }: Props) {
notFound();
}
+ const wordCount = post.content.trim().split(/\s+/).length;
+ const readingTime = Math.max(1, Math.round(wordCount / 180));
+
return (
-
-
-
}
- />
-
-
{post.title}
-
-
- {post.tags.map((tag) => (
-
- {tag.name}
-
- ))}
+
+
}
+ />
+
+
+
+
+
+
+
+
+ Anonymous thread
+
+
+ {post.title}
+
+
+ {post.tags.map((tag) => (
+
+ {tag.name}
+
+ ))}
+
+
+
+
+
+ {wordCount.toLocaleString()} words
+
+
+ {readingTime} min read
+
+
+
+
+
+
-
-
- {post.content}
-
-
+
diff --git a/bun.lock b/bun.lock
index d216d58..3bdad1d 100644
--- a/bun.lock
+++ b/bun.lock
@@ -12,12 +12,13 @@
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
- "@radix-ui/react-label": "^2.1.7",
- "@radix-ui/react-separator": "^1.1.7",
- "@radix-ui/react-slot": "^1.2.3",
- "@tanstack/react-form": "^1.23.8",
+ "@radix-ui/react-label": "^2.1.8",
+ "@radix-ui/react-separator": "^1.1.8",
+ "@radix-ui/react-slot": "^1.2.4",
+ "@radix-ui/react-tooltip": "^1.2.8",
+ "@tanstack/react-form": "^1.25.0",
"@tanstack/react-pacer": "^0.16.4",
- "@tanstack/react-query": "^5.90.5",
+ "@tanstack/react-query": "^5.90.10",
"@tanstack/react-query-devtools": "^5.90.2",
"@tanstack/react-virtual": "^3.13.12",
"arctic": "^3.7.0",
@@ -27,16 +28,19 @@
"drizzle-orm": "^0.44.7",
"drizzle-seed": "^0.3.1",
"lucide-react": "^0.548.0",
+ "motion": "^12.23.24",
"nanoid": "^5.1.6",
"next": "15.5.6",
"next-themes": "^0.4.6",
+ "ogl": "^1.0.11",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-intersection-observer": "^9.16.0",
"react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1",
"sonner": "^2.0.7",
- "tailwind-merge": "^3.3.1",
+ "svg-dotted-map": "^2.0.1",
+ "tailwind-merge": "^3.4.0",
"tailwindcss-animate": "^1.0.7",
"vaul": "^1.1.2",
"zod": "^4.1.12",
@@ -44,14 +48,14 @@
"devDependencies": {
"@biomejs/biome": "2.3.1",
"@faker-js/faker": "^10.1.0",
- "@tailwindcss/postcss": "^4.1.16",
+ "@tailwindcss/postcss": "^4.1.17",
"@tailwindcss/typography": "^0.5.19",
- "@types/node": "^22.18.12",
- "@types/react": "^19.2.2",
- "@types/react-dom": "^19.2.2",
+ "@types/node": "^22.19.1",
+ "@types/react": "^19.2.6",
+ "@types/react-dom": "^19.2.3",
"drizzle-kit": "0.31.5",
- "lefthook": "^2.0.1",
- "tailwindcss": "^4.1.16",
+ "lefthook": "^2.0.4",
+ "tailwindcss": "^4.1.17",
"typescript": "^5.9.3",
},
},
@@ -297,7 +301,7 @@
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
- "@radix-ui/react-label": ["@radix-ui/react-label@2.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ=="],
+ "@radix-ui/react-label": ["@radix-ui/react-label@2.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A=="],
"@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg=="],
@@ -311,9 +315,11 @@
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="],
- "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA=="],
+ "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g=="],
- "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+ "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
+
+ "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="],
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
@@ -331,57 +337,59 @@
"@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="],
+ "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="],
+
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
- "@tailwindcss/node": ["@tailwindcss/node@4.1.16", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.19", "source-map-js": "^1.2.1", "tailwindcss": "4.1.16" } }, "sha512-BX5iaSsloNuvKNHRN3k2RcCuTEgASTo77mofW0vmeHkfrDWaoFAFvNHpEgtu0eqyypcyiBkDWzSMxJhp3AUVcw=="],
+ "@tailwindcss/node": ["@tailwindcss/node@4.1.17", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.17" } }, "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg=="],
- "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.16", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.16", "@tailwindcss/oxide-darwin-arm64": "4.1.16", "@tailwindcss/oxide-darwin-x64": "4.1.16", "@tailwindcss/oxide-freebsd-x64": "4.1.16", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.16", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.16", "@tailwindcss/oxide-linux-arm64-musl": "4.1.16", "@tailwindcss/oxide-linux-x64-gnu": "4.1.16", "@tailwindcss/oxide-linux-x64-musl": "4.1.16", "@tailwindcss/oxide-wasm32-wasi": "4.1.16", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.16", "@tailwindcss/oxide-win32-x64-msvc": "4.1.16" } }, "sha512-2OSv52FRuhdlgyOQqgtQHuCgXnS8nFSYRp2tJ+4WZXKgTxqPy7SMSls8c3mPT5pkZ17SBToGM5LHEJBO7miEdg=="],
+ "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.17", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.17", "@tailwindcss/oxide-darwin-arm64": "4.1.17", "@tailwindcss/oxide-darwin-x64": "4.1.17", "@tailwindcss/oxide-freebsd-x64": "4.1.17", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.17", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.17", "@tailwindcss/oxide-linux-arm64-musl": "4.1.17", "@tailwindcss/oxide-linux-x64-gnu": "4.1.17", "@tailwindcss/oxide-linux-x64-musl": "4.1.17", "@tailwindcss/oxide-wasm32-wasi": "4.1.17", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.17", "@tailwindcss/oxide-win32-x64-msvc": "4.1.17" } }, "sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA=="],
- "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.16", "", { "os": "android", "cpu": "arm64" }, "sha512-8+ctzkjHgwDJ5caq9IqRSgsP70xhdhJvm+oueS/yhD5ixLhqTw9fSL1OurzMUhBwE5zK26FXLCz2f/RtkISqHA=="],
+ "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.17", "", { "os": "android", "cpu": "arm64" }, "sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ=="],
- "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.16", "", { "os": "darwin", "cpu": "arm64" }, "sha512-C3oZy5042v2FOALBZtY0JTDnGNdS6w7DxL/odvSny17ORUnaRKhyTse8xYi3yKGyfnTUOdavRCdmc8QqJYwFKA=="],
+ "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.17", "", { "os": "darwin", "cpu": "arm64" }, "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg=="],
- "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.16", "", { "os": "darwin", "cpu": "x64" }, "sha512-vjrl/1Ub9+JwU6BP0emgipGjowzYZMjbWCDqwA2Z4vCa+HBSpP4v6U2ddejcHsolsYxwL5r4bPNoamlV0xDdLg=="],
+ "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.17", "", { "os": "darwin", "cpu": "x64" }, "sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog=="],
- "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.16", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TSMpPYpQLm+aR1wW5rKuUuEruc/oOX3C7H0BTnPDn7W/eMw8W+MRMpiypKMkXZfwH8wqPIRKppuZoedTtNj2tg=="],
+ "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.17", "", { "os": "freebsd", "cpu": "x64" }, "sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g=="],
- "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.16", "", { "os": "linux", "cpu": "arm" }, "sha512-p0GGfRg/w0sdsFKBjMYvvKIiKy/LNWLWgV/plR4lUgrsxFAoQBFrXkZ4C0w8IOXfslB9vHK/JGASWD2IefIpvw=="],
+ "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17", "", { "os": "linux", "cpu": "arm" }, "sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ=="],
- "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-DoixyMmTNO19rwRPdqviTrG1rYzpxgyYJl8RgQvdAQUzxC1ToLRqtNJpU/ATURSKgIg6uerPw2feW0aS8SNr/w=="],
+ "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ=="],
- "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.16", "", { "os": "linux", "cpu": "arm64" }, "sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ=="],
+ "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg=="],
- "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.16", "", { "os": "linux", "cpu": "x64" }, "sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew=="],
+ "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.17", "", { "os": "linux", "cpu": "x64" }, "sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ=="],
- "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.16", "", { "os": "linux", "cpu": "x64" }, "sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw=="],
+ "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.17", "", { "os": "linux", "cpu": "x64" }, "sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ=="],
- "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.16", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.0.7", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q=="],
+ "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.17", "", { "dependencies": { "@emnapi/core": "^1.6.0", "@emnapi/runtime": "^1.6.0", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.0.7", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, "cpu": "none" }, "sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg=="],
- "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.16", "", { "os": "win32", "cpu": "arm64" }, "sha512-zX+Q8sSkGj6HKRTMJXuPvOcP8XfYON24zJBRPlszcH1Np7xuHXhWn8qfFjIujVzvH3BHU+16jBXwgpl20i+v9A=="],
+ "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.17", "", { "os": "win32", "cpu": "arm64" }, "sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A=="],
- "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.16", "", { "os": "win32", "cpu": "x64" }, "sha512-m5dDFJUEejbFqP+UXVstd4W/wnxA4F61q8SoL+mqTypId2T2ZpuxosNSgowiCnLp2+Z+rivdU0AqpfgiD7yCBg=="],
+ "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.17", "", { "os": "win32", "cpu": "x64" }, "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw=="],
- "@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.16", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.16", "@tailwindcss/oxide": "4.1.16", "postcss": "^8.4.41", "tailwindcss": "4.1.16" } }, "sha512-Qn3SFGPXYQMKR/UtqS+dqvPrzEeBZHrFA92maT4zijCVggdsXnDBMsPFJo1eArX3J+O+Gi+8pV4PkqjLCNBk3A=="],
+ "@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.17", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.17", "@tailwindcss/oxide": "4.1.17", "postcss": "^8.4.41", "tailwindcss": "4.1.17" } }, "sha512-+nKl9N9mN5uJ+M7dBOOCzINw94MPstNR/GtIhz1fpZysxL/4a+No64jCBD6CPN+bIHWFx3KWuu8XJRrj/572Dw=="],
"@tailwindcss/typography": ["@tailwindcss/typography@0.5.19", "", { "dependencies": { "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg=="],
- "@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.3.3", "", {}, "sha512-RfV+OPV/M3CGryYqTue684u10jUt55PEqeBOnOtCe6tAmHI9Iqyc8nHeDhWPEV9715gShuauFVaMc9RiUVNdwg=="],
+ "@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.3.5", "", {}, "sha512-RL1f5ZlfZMpghrCIdzl6mLOFLTuhqmPNblZgBaeKfdtk5rfbjykurv+VfYydOFXj0vxVIoA2d/zT7xfD7Ph8fw=="],
- "@tanstack/form-core": ["@tanstack/form-core@1.24.4", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.3.3", "@tanstack/pacer": "^0.15.3", "@tanstack/store": "^0.7.7" } }, "sha512-+eIR7DiDamit1zvTVgaHxuIRA02YFgJaXMUGxsLRJoBpUjGl/g/nhUocQoNkRyfXqOlh8OCMTanjwDprWSRq6w=="],
+ "@tanstack/form-core": ["@tanstack/form-core@1.25.0", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.3.5", "@tanstack/pacer": "^0.15.3", "@tanstack/store": "^0.7.7" } }, "sha512-OEWW2uTOFMyRmHrVEiPOn+J27ekQ/vXwRAJt9kD8U8vCt8CmpClj989OOGGSBSVJtDNxGUcWyKF8gYznnqIyaw=="],
"@tanstack/pacer": ["@tanstack/pacer@0.15.4", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.3.2", "@tanstack/store": "^0.7.5" } }, "sha512-vGY+CWsFZeac3dELgB6UZ4c7OacwsLb8hvL2gLS6hTgy8Fl0Bm/aLokHaeDIP+q9F9HUZTnp360z9uv78eg8pg=="],
- "@tanstack/query-core": ["@tanstack/query-core@5.90.5", "", {}, "sha512-wLamYp7FaDq6ZnNehypKI5fNvxHPfTYylE0m/ZpuuzJfJqhR5Pxg9gvGBHZx4n7J+V5Rg5mZxHHTlv25Zt5u+w=="],
+ "@tanstack/query-core": ["@tanstack/query-core@5.90.10", "", {}, "sha512-EhZVFu9rl7GfRNuJLJ3Y7wtbTnENsvzp+YpcAV7kCYiXni1v8qZh++lpw4ch4rrwC0u/EZRnBHIehzCGzwXDSQ=="],
"@tanstack/query-devtools": ["@tanstack/query-devtools@5.90.1", "", {}, "sha512-GtINOPjPUH0OegJExZ70UahT9ykmAhmtNVcmtdnOZbxLwT7R5OmRztR5Ahe3/Cu7LArEmR6/588tAycuaWb1xQ=="],
- "@tanstack/react-form": ["@tanstack/react-form@1.23.8", "", { "dependencies": { "@tanstack/form-core": "1.24.4", "@tanstack/react-store": "^0.7.7", "decode-formdata": "^0.9.0", "devalue": "^5.3.2" }, "peerDependencies": { "@tanstack/react-start": "^1.130.10", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@tanstack/react-start"] }, "sha512-ivfkiOHAI3aIWkCY4FnPWVAL6SkQWGWNVjtwIZpaoJE4ulukZWZ1KB8TQKs8f4STl+egjTsMHrWJuf2fv3Xh1w=="],
+ "@tanstack/react-form": ["@tanstack/react-form@1.25.0", "", { "dependencies": { "@tanstack/form-core": "1.25.0", "@tanstack/react-store": "^0.7.7" }, "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-SjKpBkjegNVW9WU+qlO8+/+kSbSEwo2zwHnrQz/yOnnJRhtdgubUt50LfeUtdzkMsbbptQ5MSZrXH03kidQjyw=="],
"@tanstack/react-pacer": ["@tanstack/react-pacer@0.16.4", "", { "dependencies": { "@tanstack/pacer": "0.15.4", "@tanstack/react-store": "^0.7.5" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-nuQLE8bx0rYMiJau4jOTPZFp3XC/GnIHDKfKVVWeKUHNF4grRdVHPgTlJ8EV/nt/HJxSUnIcy+IIKX+Bj0bLSw=="],
- "@tanstack/react-query": ["@tanstack/react-query@5.90.5", "", { "dependencies": { "@tanstack/query-core": "5.90.5" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-pN+8UWpxZkEJ/Rnnj2v2Sxpx1WFlaa9L6a4UO89p6tTQbeo+m0MS8oYDjbggrR8QcTyjKoYWKS3xJQGr3ExT8Q=="],
+ "@tanstack/react-query": ["@tanstack/react-query@5.90.10", "", { "dependencies": { "@tanstack/query-core": "5.90.10" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-BKLss9Y8PQ9IUjPYQiv3/Zmlx92uxffUOX8ZZNoQlCIZBJPT5M+GOMQj7xislvVQ6l1BstBjcX0XB/aHfFYVNw=="],
"@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.90.2", "", { "dependencies": { "@tanstack/query-devtools": "5.90.1" }, "peerDependencies": { "@tanstack/react-query": "^5.90.2", "react": "^18 || ^19" } }, "sha512-vAXJzZuBXtCQtrY3F/yUNJCV4obT/A/n81kb3+YqLbro5Z2+phdAbceO+deU3ywPw8B42oyJlp4FhO0SoivDFQ=="],
@@ -405,11 +413,11 @@
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
- "@types/node": ["@types/node@22.18.12", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-BICHQ67iqxQGFSzfCFTT7MRQ5XcBjG5aeKh5Ok38UBbPe5fxTyE+aHFxwVrGyr8GNlqFMLKD1D3P2K/1ks8tog=="],
+ "@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="],
- "@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
+ "@types/react": ["@types/react@19.2.6", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w=="],
- "@types/react-dom": ["@types/react-dom@19.2.2", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw=="],
+ "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
@@ -455,7 +463,7 @@
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
- "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
+ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
@@ -463,8 +471,6 @@
"debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
- "decode-formdata": ["decode-formdata@0.9.0", "", {}, "sha512-q5uwOjR3Um5YD+ZWPOF/1sGHVW9A5rCrRwITQChRXlmPkxDFBqCm4jNTIVdGHNH9OnR+V9MoZVgRhsFb+ARbUw=="],
-
"decode-named-character-reference": ["decode-named-character-reference@1.1.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w=="],
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
@@ -473,8 +479,6 @@
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
- "devalue": ["devalue@5.3.2", "", {}, "sha512-UDsjUbpQn9kvm68slnrs+mfxwFkIflOhkanmyabZ8zOYk8SMEIbJ3TK+88g70hSIeytu4y18f0z/hYHMTrXIWw=="],
-
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
"drizzle-kit": ["drizzle-kit@0.31.5", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-+CHgPFzuoTQTt7cOYCV6MOw2w8vqEn/ap1yv4bpZOWL03u7rlVRQhUY0WYT3rHsgVTXwYQDZaSUJSQrMBUKuWg=="],
@@ -499,6 +503,8 @@
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
+ "framer-motion": ["framer-motion@12.23.24", "", { "dependencies": { "motion-dom": "^12.23.23", "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w=="],
+
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
"get-tsconfig": ["get-tsconfig@4.10.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A=="],
@@ -529,27 +535,27 @@
"js-base64": ["js-base64@3.7.7", "", {}, "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw=="],
- "lefthook": ["lefthook@2.0.1", "", { "optionalDependencies": { "lefthook-darwin-arm64": "2.0.1", "lefthook-darwin-x64": "2.0.1", "lefthook-freebsd-arm64": "2.0.1", "lefthook-freebsd-x64": "2.0.1", "lefthook-linux-arm64": "2.0.1", "lefthook-linux-x64": "2.0.1", "lefthook-openbsd-arm64": "2.0.1", "lefthook-openbsd-x64": "2.0.1", "lefthook-windows-arm64": "2.0.1", "lefthook-windows-x64": "2.0.1" }, "bin": { "lefthook": "bin/index.js" } }, "sha512-3jL1AmEnjchHyFL9GzBaRVcfcPTQLUtXawaF6Y6MXPPCSbirTh8q/is+Ijbd1zn0FA5MwQDdSYm0guVXUkeVWg=="],
+ "lefthook": ["lefthook@2.0.4", "", { "optionalDependencies": { "lefthook-darwin-arm64": "2.0.4", "lefthook-darwin-x64": "2.0.4", "lefthook-freebsd-arm64": "2.0.4", "lefthook-freebsd-x64": "2.0.4", "lefthook-linux-arm64": "2.0.4", "lefthook-linux-x64": "2.0.4", "lefthook-openbsd-arm64": "2.0.4", "lefthook-openbsd-x64": "2.0.4", "lefthook-windows-arm64": "2.0.4", "lefthook-windows-x64": "2.0.4" }, "bin": { "lefthook": "bin/index.js" } }, "sha512-GNCU2vQWM/UWjiEF23601aILi1aMbPke6viortH7wIO/oVGOCW0H6FdLez4XZDyqnHL9XkTnd0BBVrBbYVMLpA=="],
- "lefthook-darwin-arm64": ["lefthook-darwin-arm64@2.0.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-en5tDduXeltmlbBR/wECkwkILpghR9LxexeBuHWwyTZnOunm3bk4XGg9WKwT1sWlMaKJiXl7Tv0dUB/K1d3ajg=="],
+ "lefthook-darwin-arm64": ["lefthook-darwin-arm64@2.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-AR63/O5UkM7Sc6x5PhP4vTuztTYRBeBroXApeWGM/8e5uZyoQug/7KTh7xhbCMDf8WJv6vdFeXAQCPSmDyPU3Q=="],
- "lefthook-darwin-x64": ["lefthook-darwin-x64@2.0.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-upD7B5kK3/sAqUKWfcejvWwXmBfu4LK+0bwUZWHE0cuPsn9KEZ8zG0Vw7JSBhuSZ8LeZA8PudlmQXHvR3YjDew=="],
+ "lefthook-darwin-x64": ["lefthook-darwin-x64@2.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-618DVUttSzV9egQiqTQoxGfnR240JoPWYmqRVHhiegnQKZ2lp5XJ+7NMxeRk/ih93VVOLzFO5ky3PbpxTmJgjQ=="],
- "lefthook-freebsd-arm64": ["lefthook-freebsd-arm64@2.0.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-DqraAmqgSOllSzRjhjjaToswhpz0WWfnTo7HPws5BYOLghQJ/qErwXOXs2I76YqbHgXr831wtgkamQOVOfR0Jg=="],
+ "lefthook-freebsd-arm64": ["lefthook-freebsd-arm64@2.0.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-mTAQym1BK38fKglHBQ/0GXPznVC4LoStHO5lAI3ZxaEC0FQetqGHYFzhWbIH5sde9JhztE2rL/aBzMHDoAtzSw=="],
- "lefthook-freebsd-x64": ["lefthook-freebsd-x64@2.0.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kdLmXvgWvSkFLBSIZGevkPEqoOwQIaHAtcXpbfn0unbsgqLG+u4V7Bnp73BfBUkc0ER+72W9gUas9HZALzTjEg=="],
+ "lefthook-freebsd-x64": ["lefthook-freebsd-x64@2.0.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-sy02aSxd8UMd6XmiPFVl/Em0b78jdZcDSsLwg+bweJQQk0l+vJhOfqFiG11mbnpo+EBIZmRe6OH5LkxeSU36+w=="],
- "lefthook-linux-arm64": ["lefthook-linux-arm64@2.0.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-pEQomhTh0SSeIv3G5337efm1BHQCBSrewYnUydkLFLMwkqzVzIW2119pk5bMNSKFQ2xoRs3AKww+odTgsQiLcQ=="],
+ "lefthook-linux-arm64": ["lefthook-linux-arm64@2.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-W0Nlr/Cz2QTH9n4k5zNrk3LSsg1C4wHiJi8hrAiQVTaAV/N1XrKqd0DevqQuouuapG6pw/6B1xCgiNPebv9oyw=="],
- "lefthook-linux-x64": ["lefthook-linux-x64@2.0.1", "", { "os": "linux", "cpu": "x64" }, "sha512-8tOmdiUU/awCoW/PJ25QmhHuHXUbDQ9BF3tADYuxgGSSCLtvx1Zvr+F4pyB8jCOAPvsx9XAt3UM3n8/nFXKYcw=="],
+ "lefthook-linux-x64": ["lefthook-linux-x64@2.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-N6ySVCtB/DrOZ1ZgPL8WBZTgtoVHvcPKI+LV5wbcGrvA/dzDZFvniadrbDWZg7Tm705efiQzyENjwhhqNkwiww=="],
- "lefthook-openbsd-arm64": ["lefthook-openbsd-arm64@2.0.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-SBOMOjkyM7bQTvzTlnNS5PzfhJigZJYdmdcJ/H4EadJfxl+72v0YG9SHY64AdfuvZUB39AvaBD6UothZCtLuEQ=="],
+ "lefthook-openbsd-arm64": ["lefthook-openbsd-arm64@2.0.4", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-VmOhJO3pYzZ/1C2WFXtL/n5pq4/eYOroqJJpwTJfmCHyw4ceLACu8MDyU5AMJhGMkbL8mPxGInJKxg5xhYgGRw=="],
- "lefthook-openbsd-x64": ["lefthook-openbsd-x64@2.0.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-22E1V889hx1rMOxgGFUc0ujiOAcM7kX0x7bCK6rfXFsqYl30X7A+4hj2JTwxRxSTb7ZkaiSFF64UE7iGi/f4AA=="],
+ "lefthook-openbsd-x64": ["lefthook-openbsd-x64@2.0.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-U8MZz1xlHUdflkQQ2hkMQsei6fSZbs8tuE4EjCIHWnNdnAF4V8sZ6n1KbxsJcoZXPyBZqxZSMu1o/Ye8IAMVKg=="],
- "lefthook-windows-arm64": ["lefthook-windows-arm64@2.0.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-0dDrCj0AatmJj5voABZ/7H27eZiaa1CVtTFYfKDRJj2+acEo3F7x/iadknftMqfjOjgjZpmRwSx49NWA2vMQjg=="],
+ "lefthook-windows-arm64": ["lefthook-windows-arm64@2.0.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-543H3y2JAwNdvwUQ6nlNBG7rdKgoOUgzAa6pYcl6EoqicCRrjRmGhkJu7vUudkkrD2Wjm7tr9hU9poP2g5fRFQ=="],
- "lefthook-windows-x64": ["lefthook-windows-x64@2.0.1", "", { "os": "win32", "cpu": "x64" }, "sha512-vmbqUY7lsgYiDG9b9oc0zP9b/pHx6YpS310EBv6YkVEf8mseNRKRYwm6QqvOJ919rrmZfQpOFs5W+5O9K7zleg=="],
+ "lefthook-windows-x64": ["lefthook-windows-x64@2.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-UDEPK9RWKm60xsNOdS/DQOdFba0SFa4w3tpFMXK1AJzmRHhosoKrorXGhtTr6kcM0MGKOtYi8GHsm++ArZ9wvQ=="],
"libsql": ["libsql@0.5.22", "", { "dependencies": { "@neon-rs/load": "^0.0.4", "detect-libc": "2.0.2" }, "optionalDependencies": { "@libsql/darwin-arm64": "0.5.22", "@libsql/darwin-x64": "0.5.22", "@libsql/linux-arm-gnueabihf": "0.5.22", "@libsql/linux-arm-musleabihf": "0.5.22", "@libsql/linux-arm64-gnu": "0.5.22", "@libsql/linux-arm64-musl": "0.5.22", "@libsql/linux-x64-gnu": "0.5.22", "@libsql/linux-x64-musl": "0.5.22", "@libsql/win32-x64-msvc": "0.5.22" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "arm", "x64", "arm64", ] }, "sha512-NscWthMQt7fpU8lqd7LXMvT9pi+KhhmTHAJWUB/Lj6MWa0MKFv0F2V4C6WKKpjCVZl0VwcDz4nOI3CyaT1DDiA=="],
@@ -581,7 +587,7 @@
"lucide-react": ["lucide-react@0.548.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-63b16z63jM9yc1MwxajHeuu0FRZFsDtljtDjYm26Kd86UQ5HQzu9ksEtoUUw4RBuewodw/tGFmvipePvRsKeDA=="],
- "magic-string": ["magic-string@0.30.19", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw=="],
+ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
@@ -671,6 +677,12 @@
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
+ "motion": ["motion@12.23.24", "", { "dependencies": { "framer-motion": "^12.23.24", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-Rc5E7oe2YZ72N//S3QXGzbnXgqNrTESv8KKxABR20q2FLch9gHLo0JLyYo2hZ238bZ9Gx6cWhj9VO0IgwbMjCw=="],
+
+ "motion-dom": ["motion-dom@12.23.23", "", { "dependencies": { "motion-utils": "^12.23.6" } }, "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA=="],
+
+ "motion-utils": ["motion-utils@12.23.6", "", {}, "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ=="],
+
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="],
@@ -683,6 +695,8 @@
"node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
+ "ogl": ["ogl@1.0.11", "", {}, "sha512-kUpC154AFfxi16pmZUK4jk3J+8zxwTWGPo03EoYA8QPbzikHoaC82n6pNTbd+oEaJonaE8aPWBlX7ad9zrqLsA=="],
+
"parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
@@ -747,9 +761,11 @@
"styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
- "tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="],
+ "svg-dotted-map": ["svg-dotted-map@2.0.1", "", {}, "sha512-eeI2XzIKm23gmSVr7ASTMNVJvxAvBfyL30tN33Y/DcZCJXvC/Br/cxQp9Ts6jDK/e7fkE5TpZStEfduPqPXrIw=="],
+
+ "tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="],
- "tailwindcss": ["tailwindcss@4.1.16", "", {}, "sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA=="],
+ "tailwindcss": ["tailwindcss@4.1.17", "", {}, "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q=="],
"tailwindcss-animate": ["tailwindcss-animate@1.0.7", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="],
@@ -807,9 +823,23 @@
"@oslojs/jwt/@oslojs/encoding": ["@oslojs/encoding@0.4.1", "", {}, "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q=="],
- "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.5.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg=="],
+ "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
- "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="],
+ "@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-label/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
+
+ "@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-separator/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
+
+ "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
@@ -879,6 +909,10 @@
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
+ "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core": ["@emnapi/core@1.5.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="],
+
"@types/ws/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="],
"next/postcss/nanoid": ["nanoid@3.3.8", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w=="],
@@ -890,5 +924,7 @@
"vaul/@radix-ui/react-dialog/@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA=="],
"vaul/@radix-ui/react-dialog/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "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" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA=="],
+
+ "vaul/@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
}
}
diff --git a/components.json b/components.json
index a64445d..f54adcf 100644
--- a/components.json
+++ b/components.json
@@ -10,6 +10,7 @@
"cssVariables": true,
"prefix": ""
},
+ "iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
@@ -17,5 +18,7 @@
"lib": "@/lib",
"hooks": "@/hooks"
},
- "iconLibrary": "lucide"
+ "registries": {
+ "@magicui": "https://magicui.design/r/{name}.json"
+ }
}
diff --git a/components/DecryptedText.tsx b/components/DecryptedText.tsx
new file mode 100644
index 0000000..c5ae637
--- /dev/null
+++ b/components/DecryptedText.tsx
@@ -0,0 +1,242 @@
+"use client";
+
+import { type HTMLMotionProps, motion } from "motion/react";
+import { useEffect, useRef, useState } from "react";
+
+interface DecryptedTextProps extends HTMLMotionProps<"span"> {
+ text: string;
+ speed?: number;
+ maxIterations?: number;
+ sequential?: boolean;
+ revealDirection?: "start" | "end" | "center";
+ useOriginalCharsOnly?: boolean;
+ characters?: string;
+ className?: string;
+ encryptedClassName?: string;
+ parentClassName?: string;
+ animateOn?: "view" | "hover" | "both";
+}
+
+export default function DecryptedText({
+ text,
+ speed = 50,
+ maxIterations = 10,
+ sequential = false,
+ revealDirection = "start",
+ useOriginalCharsOnly = false,
+ characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&*()_+",
+ className = "",
+ parentClassName = "",
+ encryptedClassName = "",
+ animateOn = "hover",
+ ...props
+}: DecryptedTextProps) {
+ const [displayText, setDisplayText] = useState(text);
+ const [isHovering, setIsHovering] = useState(false);
+ const [isScrambling, setIsScrambling] = useState(false);
+ const [revealedIndices, setRevealedIndices] = useState>(
+ new Set(),
+ );
+ const [hasAnimated, setHasAnimated] = useState(false);
+ const containerRef = useRef(null);
+
+ useEffect(() => {
+ let interval: NodeJS.Timeout;
+ let currentIteration = 0;
+
+ const getNextIndex = (revealedSet: Set): number => {
+ const textLength = text.length;
+ switch (revealDirection) {
+ case "start":
+ return revealedSet.size;
+ case "end":
+ return textLength - 1 - revealedSet.size;
+ case "center": {
+ const middle = Math.floor(textLength / 2);
+ const offset = Math.floor(revealedSet.size / 2);
+ const nextIndex =
+ revealedSet.size % 2 === 0 ? middle + offset : middle - offset - 1;
+
+ if (
+ nextIndex >= 0 &&
+ nextIndex < textLength &&
+ !revealedSet.has(nextIndex)
+ ) {
+ return nextIndex;
+ }
+ for (let i = 0; i < textLength; i++) {
+ if (!revealedSet.has(i)) return i;
+ }
+ return 0;
+ }
+ default:
+ return revealedSet.size;
+ }
+ };
+
+ const availableChars = useOriginalCharsOnly
+ ? Array.from(new Set(text.split(""))).filter((char) => char !== " ")
+ : characters.split("");
+
+ const shuffleText = (
+ originalText: string,
+ currentRevealed: Set,
+ ): string => {
+ if (useOriginalCharsOnly) {
+ const positions = originalText.split("").map((char, i) => ({
+ char,
+ isSpace: char === " ",
+ index: i,
+ isRevealed: currentRevealed.has(i),
+ }));
+
+ const nonSpaceChars = positions
+ .filter((p) => !p.isSpace && !p.isRevealed)
+ .map((p) => p.char);
+
+ 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 originalText[p.index];
+ return nonSpaceChars[charIndex++];
+ })
+ .join("");
+ } else {
+ return originalText
+ .split("")
+ .map((char, i) => {
+ if (char === " ") return " ";
+ if (currentRevealed.has(i)) return originalText[i];
+ return availableChars[
+ Math.floor(Math.random() * availableChars.length)
+ ];
+ })
+ .join("");
+ }
+ };
+
+ if (isHovering) {
+ setIsScrambling(true);
+ interval = setInterval(() => {
+ setRevealedIndices((prevRevealed) => {
+ if (sequential) {
+ if (prevRevealed.size < text.length) {
+ const nextIndex = getNextIndex(prevRevealed);
+ const newRevealed = new Set(prevRevealed);
+ newRevealed.add(nextIndex);
+ setDisplayText(shuffleText(text, newRevealed));
+ return newRevealed;
+ } else {
+ clearInterval(interval);
+ setIsScrambling(false);
+ return prevRevealed;
+ }
+ } else {
+ setDisplayText(shuffleText(text, prevRevealed));
+ currentIteration++;
+ if (currentIteration >= maxIterations) {
+ clearInterval(interval);
+ setIsScrambling(false);
+ setDisplayText(text);
+ }
+ return prevRevealed;
+ }
+ });
+ }, speed);
+ } else {
+ setDisplayText(text);
+ setRevealedIndices(new Set());
+ setIsScrambling(false);
+ }
+
+ return () => {
+ if (interval) clearInterval(interval);
+ };
+ }, [
+ isHovering,
+ text,
+ speed,
+ maxIterations,
+ sequential,
+ revealDirection,
+ characters,
+ useOriginalCharsOnly,
+ ]);
+
+ useEffect(() => {
+ if (animateOn !== "view" && animateOn !== "both") return;
+
+ const observerCallback = (entries: IntersectionObserverEntry[]) => {
+ entries.forEach((entry) => {
+ if (entry.isIntersecting && !hasAnimated) {
+ setIsHovering(true);
+ setHasAnimated(true);
+ }
+ });
+ };
+
+ const observerOptions = {
+ root: null,
+ rootMargin: "0px",
+ threshold: 0.1,
+ };
+
+ const observer = new IntersectionObserver(
+ observerCallback,
+ observerOptions,
+ );
+ const currentRef = containerRef.current;
+ if (currentRef) {
+ observer.observe(currentRef);
+ }
+
+ return () => {
+ if (currentRef) observer.unobserve(currentRef);
+ };
+ }, [animateOn, hasAnimated]);
+
+ const hoverProps =
+ animateOn === "hover" || animateOn === "both"
+ ? {
+ onMouseEnter: () => setIsHovering(true),
+ onMouseLeave: () => setIsHovering(false),
+ }
+ : {};
+
+ return (
+
+ {displayText}
+
+
+ {displayText.split("").map((char, index) => {
+ const isRevealedOrDone =
+ revealedIndices.has(index) || !isScrambling || !isHovering;
+
+ return (
+
+ key={index}
+ className={isRevealedOrDone ? className : encryptedClassName}
+ >
+ {char}
+
+ );
+ })}
+
+
+ );
+}
diff --git a/components/LightRays.tsx b/components/LightRays.tsx
new file mode 100644
index 0000000..208530e
--- /dev/null
+++ b/components/LightRays.tsx
@@ -0,0 +1,450 @@
+"use client";
+
+import { Mesh, Program, Renderer, Triangle } from "ogl";
+import { useEffect, useRef, useState } from "react";
+
+export type RaysOrigin =
+ | "top-center"
+ | "top-left"
+ | "top-right"
+ | "right"
+ | "left"
+ | "bottom-center"
+ | "bottom-right"
+ | "bottom-left";
+
+interface LightRaysProps {
+ raysOrigin?: RaysOrigin;
+ raysColor?: string;
+ raysSpeed?: number;
+ lightSpread?: number;
+ rayLength?: number;
+ pulsating?: boolean;
+ fadeDistance?: number;
+ saturation?: number;
+ followMouse?: boolean;
+ mouseInfluence?: number;
+ noiseAmount?: number;
+ distortion?: number;
+ className?: string;
+}
+
+const DEFAULT_COLOR = "#ffffff";
+
+const hexToRgb = (hex: string): [number, number, number] => {
+ const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+ return m
+ ? [
+ parseInt(m[1], 16) / 255,
+ parseInt(m[2], 16) / 255,
+ parseInt(m[3], 16) / 255,
+ ]
+ : [1, 1, 1];
+};
+
+const getAnchorAndDir = (
+ origin: RaysOrigin,
+ w: number,
+ h: number,
+): { anchor: [number, number]; dir: [number, number] } => {
+ const outside = 0.2;
+ switch (origin) {
+ case "top-left":
+ return { anchor: [0, -outside * h], dir: [0, 1] };
+ case "top-right":
+ return { anchor: [w, -outside * h], dir: [0, 1] };
+ case "left":
+ return { anchor: [-outside * w, 0.5 * h], dir: [1, 0] };
+ case "right":
+ return { anchor: [(1 + outside) * w, 0.5 * h], dir: [-1, 0] };
+ case "bottom-left":
+ return { anchor: [0, (1 + outside) * h], dir: [0, -1] };
+ case "bottom-center":
+ return { anchor: [0.5 * w, (1 + outside) * h], dir: [0, -1] };
+ case "bottom-right":
+ return { anchor: [w, (1 + outside) * h], dir: [0, -1] };
+ default: // "top-center"
+ return { anchor: [0.5 * w, -outside * h], dir: [0, 1] };
+ }
+};
+
+const LightRays: React.FC = ({
+ raysOrigin = "top-center",
+ raysColor = DEFAULT_COLOR,
+ raysSpeed = 1,
+ lightSpread = 1,
+ rayLength = 2,
+ pulsating = false,
+ fadeDistance = 1.0,
+ saturation = 1.0,
+ followMouse = true,
+ mouseInfluence = 0.1,
+ noiseAmount = 0.0,
+ distortion = 0.0,
+ className = "",
+}) => {
+ const containerRef = useRef(null);
+ const uniformsRef = useRef(null);
+ const rendererRef = useRef(null);
+ const mouseRef = useRef({ x: 0.5, y: 0.5 });
+ const smoothMouseRef = useRef({ x: 0.5, y: 0.5 });
+ const animationIdRef = useRef(null);
+ const meshRef = useRef(null);
+ const cleanupFunctionRef = useRef<(() => void) | null>(null);
+ const [isVisible, setIsVisible] = useState(false);
+ const observerRef = useRef(null);
+
+ useEffect(() => {
+ if (!containerRef.current) return;
+
+ observerRef.current = new IntersectionObserver(
+ (entries) => {
+ const entry = entries[0];
+ setIsVisible(entry.isIntersecting);
+ },
+ { threshold: 0.1 },
+ );
+
+ observerRef.current.observe(containerRef.current);
+
+ return () => {
+ if (observerRef.current) {
+ observerRef.current.disconnect();
+ observerRef.current = null;
+ }
+ };
+ }, []);
+
+ useEffect(() => {
+ if (!isVisible || !containerRef.current) return;
+
+ if (cleanupFunctionRef.current) {
+ cleanupFunctionRef.current();
+ cleanupFunctionRef.current = null;
+ }
+
+ const initializeWebGL = async () => {
+ if (!containerRef.current) return;
+
+ await new Promise((resolve) => setTimeout(resolve, 10));
+
+ if (!containerRef.current) return;
+
+ const renderer = new Renderer({
+ dpr: Math.min(window.devicePixelRatio, 2),
+ alpha: true,
+ });
+ rendererRef.current = renderer;
+
+ const gl = renderer.gl;
+ gl.canvas.style.width = "100%";
+ gl.canvas.style.height = "100%";
+
+ while (containerRef.current.firstChild) {
+ containerRef.current.removeChild(containerRef.current.firstChild);
+ }
+ containerRef.current.appendChild(gl.canvas);
+
+ const vert = `
+attribute vec2 position;
+varying vec2 vUv;
+void main() {
+ vUv = position * 0.5 + 0.5;
+ gl_Position = vec4(position, 0.0, 1.0);
+}`;
+
+ const frag = `precision highp float;
+
+uniform float iTime;
+uniform vec2 iResolution;
+
+uniform vec2 rayPos;
+uniform vec2 rayDir;
+uniform vec3 raysColor;
+uniform float raysSpeed;
+uniform float lightSpread;
+uniform float rayLength;
+uniform float pulsating;
+uniform float fadeDistance;
+uniform float saturation;
+uniform vec2 mousePos;
+uniform float mouseInfluence;
+uniform float noiseAmount;
+uniform float distortion;
+
+varying vec2 vUv;
+
+float noise(vec2 st) {
+ return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123);
+}
+
+float rayStrength(vec2 raySource, vec2 rayRefDirection, vec2 coord,
+ float seedA, float seedB, float speed) {
+ vec2 sourceToCoord = coord - raySource;
+ vec2 dirNorm = normalize(sourceToCoord);
+ float cosAngle = dot(dirNorm, rayRefDirection);
+
+ float distortedAngle = cosAngle + distortion * sin(iTime * 2.0 + length(sourceToCoord) * 0.01) * 0.2;
+
+ float spreadFactor = pow(max(distortedAngle, 0.0), 1.0 / max(lightSpread, 0.001));
+
+ float distance = length(sourceToCoord);
+ float maxDistance = iResolution.x * rayLength;
+ float lengthFalloff = clamp((maxDistance - distance) / maxDistance, 0.0, 1.0);
+
+ float fadeFalloff = clamp((iResolution.x * fadeDistance - distance) / (iResolution.x * fadeDistance), 0.5, 1.0);
+ float pulse = pulsating > 0.5 ? (0.8 + 0.2 * sin(iTime * speed * 3.0)) : 1.0;
+
+ float baseStrength = clamp(
+ (0.45 + 0.15 * sin(distortedAngle * seedA + iTime * speed)) +
+ (0.3 + 0.2 * cos(-distortedAngle * seedB + iTime * speed)),
+ 0.0, 1.0
+ );
+
+ return baseStrength * lengthFalloff * fadeFalloff * spreadFactor * pulse;
+}
+
+void mainImage(out vec4 fragColor, in vec2 fragCoord) {
+ vec2 coord = vec2(fragCoord.x, iResolution.y - fragCoord.y);
+
+ vec2 finalRayDir = rayDir;
+ if (mouseInfluence > 0.0) {
+ vec2 mouseScreenPos = mousePos * iResolution.xy;
+ vec2 mouseDirection = normalize(mouseScreenPos - rayPos);
+ finalRayDir = normalize(mix(rayDir, mouseDirection, mouseInfluence));
+ }
+
+ vec4 rays1 = vec4(1.0) *
+ rayStrength(rayPos, finalRayDir, coord, 36.2214, 21.11349,
+ 1.5 * raysSpeed);
+ vec4 rays2 = vec4(1.0) *
+ rayStrength(rayPos, finalRayDir, coord, 22.3991, 18.0234,
+ 1.1 * raysSpeed);
+
+ fragColor = rays1 * 0.5 + rays2 * 0.4;
+
+ if (noiseAmount > 0.0) {
+ float n = noise(coord * 0.01 + iTime * 0.1);
+ fragColor.rgb *= (1.0 - noiseAmount + noiseAmount * n);
+ }
+
+ float brightness = 1.0 - (coord.y / iResolution.y);
+ fragColor.x *= 0.1 + brightness * 0.8;
+ fragColor.y *= 0.3 + brightness * 0.6;
+ fragColor.z *= 0.5 + brightness * 0.5;
+
+ if (saturation != 1.0) {
+ float gray = dot(fragColor.rgb, vec3(0.299, 0.587, 0.114));
+ fragColor.rgb = mix(vec3(gray), fragColor.rgb, saturation);
+ }
+
+ fragColor.rgb *= raysColor;
+}
+
+void main() {
+ vec4 color;
+ mainImage(color, gl_FragCoord.xy);
+ gl_FragColor = color;
+}`;
+
+ const uniforms = {
+ iTime: { value: 0 },
+ iResolution: { value: [1, 1] },
+
+ rayPos: { value: [0, 0] },
+ rayDir: { value: [0, 1] },
+
+ raysColor: { value: hexToRgb(raysColor) },
+ raysSpeed: { value: raysSpeed },
+ lightSpread: { value: lightSpread },
+ rayLength: { value: rayLength },
+ pulsating: { value: pulsating ? 1.0 : 0.0 },
+ fadeDistance: { value: fadeDistance },
+ saturation: { value: saturation },
+ mousePos: { value: [0.5, 0.5] },
+ mouseInfluence: { value: mouseInfluence },
+ noiseAmount: { value: noiseAmount },
+ distortion: { value: distortion },
+ };
+ uniformsRef.current = uniforms;
+
+ const geometry = new Triangle(gl);
+ const program = new Program(gl, {
+ vertex: vert,
+ fragment: frag,
+ uniforms,
+ });
+ const mesh = new Mesh(gl, { geometry, program });
+ meshRef.current = mesh;
+
+ const updatePlacement = () => {
+ if (!containerRef.current || !renderer) return;
+
+ renderer.dpr = Math.min(window.devicePixelRatio, 2);
+
+ const { clientWidth: wCSS, clientHeight: hCSS } = containerRef.current;
+ renderer.setSize(wCSS, hCSS);
+
+ const dpr = renderer.dpr;
+ const w = wCSS * dpr;
+ const h = hCSS * dpr;
+
+ uniforms.iResolution.value = [w, h];
+
+ const { anchor, dir } = getAnchorAndDir(raysOrigin, w, h);
+ uniforms.rayPos.value = anchor;
+ uniforms.rayDir.value = dir;
+ };
+
+ const loop = (t: number) => {
+ if (!rendererRef.current || !uniformsRef.current || !meshRef.current) {
+ return;
+ }
+
+ uniforms.iTime.value = t * 0.001;
+
+ if (followMouse && mouseInfluence > 0.0) {
+ const smoothing = 0.92;
+
+ smoothMouseRef.current.x =
+ smoothMouseRef.current.x * smoothing +
+ mouseRef.current.x * (1 - smoothing);
+ smoothMouseRef.current.y =
+ smoothMouseRef.current.y * smoothing +
+ mouseRef.current.y * (1 - smoothing);
+
+ uniforms.mousePos.value = [
+ smoothMouseRef.current.x,
+ smoothMouseRef.current.y,
+ ];
+ }
+
+ try {
+ renderer.render({ scene: mesh });
+ animationIdRef.current = requestAnimationFrame(loop);
+ } catch (error) {
+ console.warn("WebGL rendering error:", error);
+ return;
+ }
+ };
+
+ window.addEventListener("resize", updatePlacement);
+ updatePlacement();
+ animationIdRef.current = requestAnimationFrame(loop);
+
+ cleanupFunctionRef.current = () => {
+ if (animationIdRef.current) {
+ cancelAnimationFrame(animationIdRef.current);
+ animationIdRef.current = null;
+ }
+
+ window.removeEventListener("resize", updatePlacement);
+
+ if (renderer) {
+ try {
+ const canvas = renderer.gl.canvas;
+ const loseContextExt =
+ renderer.gl.getExtension("WEBGL_lose_context");
+ if (loseContextExt) {
+ loseContextExt.loseContext();
+ }
+
+ if (canvas && canvas.parentNode) {
+ canvas.parentNode.removeChild(canvas);
+ }
+ } catch (error) {
+ console.warn("Error during WebGL cleanup:", error);
+ }
+ }
+
+ rendererRef.current = null;
+ uniformsRef.current = null;
+ meshRef.current = null;
+ };
+ };
+
+ initializeWebGL();
+
+ return () => {
+ if (cleanupFunctionRef.current) {
+ cleanupFunctionRef.current();
+ cleanupFunctionRef.current = null;
+ }
+ };
+ }, [
+ isVisible,
+ raysOrigin,
+ raysColor,
+ raysSpeed,
+ lightSpread,
+ rayLength,
+ pulsating,
+ fadeDistance,
+ saturation,
+ followMouse,
+ mouseInfluence,
+ noiseAmount,
+ distortion,
+ ]);
+
+ useEffect(() => {
+ if (!uniformsRef.current || !containerRef.current || !rendererRef.current)
+ return;
+
+ const u = uniformsRef.current;
+ const renderer = rendererRef.current;
+
+ u.raysColor.value = hexToRgb(raysColor);
+ u.raysSpeed.value = raysSpeed;
+ u.lightSpread.value = lightSpread;
+ u.rayLength.value = rayLength;
+ u.pulsating.value = pulsating ? 1.0 : 0.0;
+ u.fadeDistance.value = fadeDistance;
+ u.saturation.value = saturation;
+ u.mouseInfluence.value = mouseInfluence;
+ u.noiseAmount.value = noiseAmount;
+ u.distortion.value = distortion;
+
+ const { clientWidth: wCSS, clientHeight: hCSS } = containerRef.current;
+ const dpr = renderer.dpr;
+ const { anchor, dir } = getAnchorAndDir(raysOrigin, wCSS * dpr, hCSS * dpr);
+ u.rayPos.value = anchor;
+ u.rayDir.value = dir;
+ }, [
+ raysColor,
+ raysSpeed,
+ lightSpread,
+ raysOrigin,
+ rayLength,
+ pulsating,
+ fadeDistance,
+ saturation,
+ mouseInfluence,
+ noiseAmount,
+ distortion,
+ ]);
+
+ useEffect(() => {
+ const handleMouseMove = (e: MouseEvent) => {
+ if (!containerRef.current || !rendererRef.current) return;
+ const rect = containerRef.current.getBoundingClientRect();
+ const x = (e.clientX - rect.left) / rect.width;
+ const y = (e.clientY - rect.top) / rect.height;
+ mouseRef.current = { x, y };
+ };
+
+ if (followMouse) {
+ window.addEventListener("mousemove", handleMouseMove);
+ return () => window.removeEventListener("mousemove", handleMouseMove);
+ }
+ }, [followMouse]);
+
+ return (
+
+ );
+};
+
+export default LightRays;
diff --git a/components/ShinyText.tsx b/components/ShinyText.tsx
new file mode 100644
index 0000000..26e0fb9
--- /dev/null
+++ b/components/ShinyText.tsx
@@ -0,0 +1,34 @@
+import type React from "react";
+
+interface ShinyTextProps {
+ text: string;
+ disabled?: boolean;
+ duration?: number;
+ className?: string;
+}
+
+const ShinyText: React.FC = ({
+ text,
+ disabled = false,
+ duration = 5,
+ className = "",
+}) => {
+ const animationDuration = `${duration}s`;
+
+ return (
+
+ {text}
+
+ );
+};
+
+export default ShinyText;
diff --git a/components/dock-nav.tsx b/components/dock-nav.tsx
new file mode 100644
index 0000000..31eb46e
--- /dev/null
+++ b/components/dock-nav.tsx
@@ -0,0 +1,114 @@
+"use client";
+
+import { HomeIcon, InfoIcon, MessageCirclePlusIcon } from "lucide-react";
+import Link from "next/link";
+import type React from "react";
+import { LogoutButton } from "@/app/forum/components/logout-button";
+import { buttonVariants } from "@/components/ui/button";
+import { Separator } from "@/components/ui/separator";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+import { cn } from "@/lib/utils";
+import { Dock, DockIcon } from "./ui/dock";
+
+export type IconProps = React.HTMLAttributes;
+
+const Icons = {
+ github: (props: IconProps) => (
+
+
+
+ ),
+};
+
+const DATA = {
+ navbar: [
+ { href: "/", icon: HomeIcon, label: "Home" },
+ { href: "/about", icon: InfoIcon, label: "About" },
+ {
+ href: "/forum/submit",
+ icon: MessageCirclePlusIcon,
+ label: "Create Post",
+ },
+ ],
+ secondary: {
+ Source: {
+ name: "Source code",
+ url: "https://github.com/omsimos/umedu",
+ icon: Icons.github,
+ },
+ },
+};
+
+export function DockNav({ logout }: { logout: () => Promise }) {
+ return (
+
+
+
+ {DATA.navbar.map((item) => (
+
+
+
+
+
+
+
+
+ {item.label}
+
+
+
+ ))}
+
+ {Object.entries(DATA.secondary).map(([name, social]) => (
+
+
+
+
+
+
+
+
+ {name}
+
+
+
+ ))}
+
+
+
+
+
+
+ Logout
+
+
+
+
+
+
+ );
+}
diff --git a/components/flat-map.tsx b/components/flat-map.tsx
new file mode 100644
index 0000000..4e24b12
--- /dev/null
+++ b/components/flat-map.tsx
@@ -0,0 +1,88 @@
+import { DottedMap } from "./ui/dotted-map";
+
+const markers = [
+ {
+ lat: 40.7128,
+ lng: -74.006,
+ size: 0.3,
+ }, // New York
+ {
+ lat: 34.0522,
+ lng: -118.2437,
+ size: 0.3,
+ }, // Los Angeles
+ {
+ lat: 51.5074,
+ lng: -0.1278,
+ size: 0.3,
+ }, // London
+ {
+ lat: -33.8688,
+ lng: 151.2093,
+ size: 0.3,
+ }, // Sydney
+ {
+ lat: 48.8566,
+ lng: 2.3522,
+ size: 0.3,
+ }, // Paris
+ {
+ lat: 35.6762,
+ lng: 139.6503,
+ size: 0.3,
+ }, // Tokyo
+ {
+ lat: 55.7558,
+ lng: 37.6176,
+ size: 0.3,
+ }, // Moscow
+ {
+ lat: 39.9042,
+ lng: 116.4074,
+ size: 0.3,
+ }, // Beijing
+ {
+ lat: 28.6139,
+ lng: 77.209,
+ size: 0.3,
+ }, // New Delhi
+ {
+ lat: -23.5505,
+ lng: -46.6333,
+ size: 0.3,
+ }, // São Paulo
+ {
+ lat: 1.3521,
+ lng: 103.8198,
+ size: 0.3,
+ }, // Singapore
+ {
+ lat: 25.2048,
+ lng: 55.2708,
+ size: 0.3,
+ }, // Dubai
+ {
+ lat: 52.52,
+ lng: 13.405,
+ size: 0.3,
+ }, // Berlin
+ {
+ lat: 19.4326,
+ lng: -99.1332,
+ size: 0.3,
+ }, // Mexico City
+ {
+ lat: -26.2041,
+ lng: 28.0473,
+ size: 0.3,
+ }, // Johannesburg
+];
+
+export function FlatMap() {
+ return (
+
+ );
+}
diff --git a/components/footer.tsx b/components/footer.tsx
index 71550b3..f5115ea 100644
--- a/components/footer.tsx
+++ b/components/footer.tsx
@@ -8,8 +8,8 @@ export function Footer() {
umedu
{" "}
by{" "}
-
- @josh.xfi
+
+ @omsimos
diff --git a/components/ui/dock.tsx b/components/ui/dock.tsx
new file mode 100644
index 0000000..29eaef8
--- /dev/null
+++ b/components/ui/dock.tsx
@@ -0,0 +1,153 @@
+"use client";
+
+import { cva, type VariantProps } from "class-variance-authority";
+import type { MotionProps } from "motion/react";
+import {
+ type MotionValue,
+ motion,
+ useMotionValue,
+ useSpring,
+ useTransform,
+} from "motion/react";
+import React, { type PropsWithChildren, useRef } from "react";
+
+import { cn } from "@/lib/utils";
+
+export interface DockProps extends VariantProps
{
+ className?: string;
+ iconSize?: number;
+ iconMagnification?: number;
+ disableMagnification?: boolean;
+ iconDistance?: number;
+ direction?: "top" | "middle" | "bottom";
+ children: React.ReactNode;
+}
+
+const DEFAULT_SIZE = 40;
+const DEFAULT_MAGNIFICATION = 60;
+const DEFAULT_DISTANCE = 140;
+const DEFAULT_DISABLEMAGNIFICATION = false;
+
+const dockVariants = cva(
+ "supports-backdrop-blur:bg-white/10 supports-backdrop-blur:dark:bg-black/10 mx-auto mt-8 flex h-[58px] w-max items-center justify-center gap-2 rounded-2xl border p-2 backdrop-blur-md",
+);
+
+const Dock = React.forwardRef(
+ (
+ {
+ className,
+ children,
+ iconSize = DEFAULT_SIZE,
+ iconMagnification = DEFAULT_MAGNIFICATION,
+ disableMagnification = DEFAULT_DISABLEMAGNIFICATION,
+ iconDistance = DEFAULT_DISTANCE,
+ direction = "middle",
+ ...props
+ },
+ ref,
+ ) => {
+ const mouseX = useMotionValue(Infinity);
+
+ const renderChildren = () => {
+ return React.Children.map(children, (child) => {
+ if (
+ React.isValidElement(child) &&
+ child.type === DockIcon
+ ) {
+ return React.cloneElement(child, {
+ ...child.props,
+ mouseX: mouseX,
+ size: iconSize,
+ magnification: iconMagnification,
+ disableMagnification: disableMagnification,
+ distance: iconDistance,
+ });
+ }
+ return child;
+ });
+ };
+
+ return (
+ mouseX.set(e.pageX)}
+ onMouseLeave={() => mouseX.set(Infinity)}
+ {...props}
+ className={cn(dockVariants({ className }), {
+ "items-start": direction === "top",
+ "items-center": direction === "middle",
+ "items-end": direction === "bottom",
+ })}
+ >
+ {renderChildren()}
+
+ );
+ },
+);
+
+Dock.displayName = "Dock";
+
+export interface DockIconProps
+ extends Omit, "children"> {
+ size?: number;
+ magnification?: number;
+ disableMagnification?: boolean;
+ distance?: number;
+ mouseX?: MotionValue;
+ className?: string;
+ children?: React.ReactNode;
+ props?: PropsWithChildren;
+}
+
+const DockIcon = ({
+ size = DEFAULT_SIZE,
+ magnification = DEFAULT_MAGNIFICATION,
+ disableMagnification,
+ distance = DEFAULT_DISTANCE,
+ mouseX,
+ className,
+ children,
+ ...props
+}: DockIconProps) => {
+ const ref = useRef(null);
+ const padding = Math.max(6, size * 0.2);
+ const defaultMouseX = useMotionValue(Infinity);
+
+ const distanceCalc = useTransform(mouseX ?? defaultMouseX, (val: number) => {
+ const bounds = ref.current?.getBoundingClientRect() ?? { x: 0, width: 0 };
+ return val - bounds.x - bounds.width / 2;
+ });
+
+ const targetSize = disableMagnification ? size : magnification;
+
+ const sizeTransform = useTransform(
+ distanceCalc,
+ [-distance, 0, distance],
+ [size, targetSize, size],
+ );
+
+ const scaleSize = useSpring(sizeTransform, {
+ mass: 0.1,
+ stiffness: 150,
+ damping: 12,
+ });
+
+ return (
+
+ {children}
+
+ );
+};
+
+DockIcon.displayName = "DockIcon";
+
+export { Dock, DockIcon, dockVariants };
diff --git a/components/ui/dotted-map.tsx b/components/ui/dotted-map.tsx
new file mode 100644
index 0000000..ecf8642
--- /dev/null
+++ b/components/ui/dotted-map.tsx
@@ -0,0 +1,103 @@
+import * as React from "react";
+import { createMap } from "svg-dotted-map";
+
+import { cn } from "@/lib/utils";
+
+interface Marker {
+ lat: number;
+ lng: number;
+ size?: number;
+}
+
+export interface DottedMapProps extends React.SVGProps {
+ width?: number;
+ height?: number;
+ mapSamples?: number;
+ markers?: Marker[];
+ dotColor?: string;
+ markerColor?: string;
+ dotRadius?: number;
+ stagger?: boolean;
+}
+
+export function DottedMap({
+ width = 150,
+ height = 75,
+ mapSamples = 5000,
+ markers = [],
+ markerColor = "#FF6900",
+ dotRadius = 0.2,
+ stagger = true,
+ className,
+ style,
+}: DottedMapProps) {
+ const { points, addMarkers } = createMap({
+ width,
+ height,
+ mapSamples,
+ });
+
+ const processedMarkers = addMarkers(markers);
+
+ // Compute stagger helpers in a single, simple pass
+ const { xStep, yToRowIndex } = React.useMemo(() => {
+ const sorted = [...points].sort((a, b) => a.y - b.y || a.x - b.x);
+ const rowMap = new Map();
+ let step = 0;
+ let prevY = Number.NaN;
+ let prevXInRow = Number.NaN;
+
+ for (const p of sorted) {
+ if (p.y !== prevY) {
+ // new row
+ prevY = p.y;
+ prevXInRow = Number.NaN;
+ if (!rowMap.has(p.y)) rowMap.set(p.y, rowMap.size);
+ }
+ if (!Number.isNaN(prevXInRow)) {
+ const delta = p.x - prevXInRow;
+ if (delta > 0) step = step === 0 ? delta : Math.min(step, delta);
+ }
+ prevXInRow = p.x;
+ }
+
+ return { xStep: step || 1, yToRowIndex: rowMap };
+ }, [points]);
+
+ return (
+
+ {points.map((point, index) => {
+ const rowIndex = yToRowIndex.get(point.y) ?? 0;
+ const offsetX = stagger && rowIndex % 2 === 1 ? xStep / 2 : 0;
+ return (
+
+ );
+ })}
+ {processedMarkers.map((marker, index) => {
+ const rowIndex = yToRowIndex.get(marker.y) ?? 0;
+ const offsetX = stagger && rowIndex % 2 === 1 ? xStep / 2 : 0;
+ return (
+
+ );
+ })}
+
+ );
+}
diff --git a/components/ui/progressive-blur.tsx b/components/ui/progressive-blur.tsx
new file mode 100644
index 0000000..65bf888
--- /dev/null
+++ b/components/ui/progressive-blur.tsx
@@ -0,0 +1,116 @@
+"use client";
+
+import type React from "react";
+
+import { cn } from "@/lib/utils";
+
+export interface ProgressiveBlurProps {
+ className?: string;
+ height?: string;
+ position?: "top" | "bottom" | "both";
+ blurLevels?: number[];
+ children?: React.ReactNode;
+}
+
+export function ProgressiveBlur({
+ className,
+ height = "30%",
+ position = "bottom",
+ blurLevels = [0.5, 1, 2, 4, 8, 16, 32, 64],
+}: ProgressiveBlurProps) {
+ // Create array with length equal to blurLevels.length - 2 (for before/after pseudo elements)
+ const divElements = Array(blurLevels.length - 2).fill(null);
+
+ return (
+
+ {/* First blur layer (pseudo element) */}
+
+
+ {/* Middle blur layers */}
+ {divElements.map((_, index) => {
+ const blurIndex = index + 1;
+ const startPercent = blurIndex * 12.5;
+ const midPercent = (blurIndex + 1) * 12.5;
+ const endPercent = (blurIndex + 2) * 12.5;
+
+ const maskGradient =
+ position === "bottom"
+ ? `linear-gradient(to bottom, rgba(0,0,0,0) ${startPercent}%, rgba(0,0,0,1) ${midPercent}%, rgba(0,0,0,1) ${endPercent}%, rgba(0,0,0,0) ${endPercent + 12.5}%)`
+ : position === "top"
+ ? `linear-gradient(to top, rgba(0,0,0,0) ${startPercent}%, rgba(0,0,0,1) ${midPercent}%, rgba(0,0,0,1) ${endPercent}%, rgba(0,0,0,0) ${endPercent + 12.5}%)`
+ : `linear-gradient(rgba(0,0,0,0) 0%, rgba(0,0,0,1) 5%, rgba(0,0,0,1) 95%, rgba(0,0,0,0) 100%)`;
+
+ return (
+
+ index
+ }`}
+ className="absolute inset-0"
+ style={{
+ zIndex: index + 2,
+ backdropFilter: `blur(${blurLevels[blurIndex]}px)`,
+ WebkitBackdropFilter: `blur(${blurLevels[blurIndex]}px)`,
+ maskImage: maskGradient,
+ WebkitMaskImage: maskGradient,
+ }}
+ />
+ );
+ })}
+
+ {/* Last blur layer (pseudo element) */}
+
+
+ );
+}
diff --git a/components/ui/tooltip.tsx b/components/ui/tooltip.tsx
new file mode 100644
index 0000000..ed700c2
--- /dev/null
+++ b/components/ui/tooltip.tsx
@@ -0,0 +1,61 @@
+"use client";
+
+import * as TooltipPrimitive from "@radix-ui/react-tooltip";
+import type * as React from "react";
+
+import { cn } from "@/lib/utils";
+
+function TooltipProvider({
+ delayDuration = 0,
+ ...props
+}: React.ComponentProps
) {
+ return (
+
+ );
+}
+
+function Tooltip({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ );
+}
+
+function TooltipTrigger({
+ ...props
+}: React.ComponentProps) {
+ return ;
+}
+
+function TooltipContent({
+ className,
+ sideOffset = 0,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+ {children}
+
+
+
+ );
+}
+
+export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
diff --git a/package.json b/package.json
index 4f0aa01..571fd79 100644
--- a/package.json
+++ b/package.json
@@ -25,12 +25,13 @@
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
- "@radix-ui/react-label": "^2.1.7",
- "@radix-ui/react-separator": "^1.1.7",
- "@radix-ui/react-slot": "^1.2.3",
- "@tanstack/react-form": "^1.23.8",
+ "@radix-ui/react-label": "^2.1.8",
+ "@radix-ui/react-separator": "^1.1.8",
+ "@radix-ui/react-slot": "^1.2.4",
+ "@radix-ui/react-tooltip": "^1.2.8",
+ "@tanstack/react-form": "^1.25.0",
"@tanstack/react-pacer": "^0.16.4",
- "@tanstack/react-query": "^5.90.5",
+ "@tanstack/react-query": "^5.90.10",
"@tanstack/react-query-devtools": "^5.90.2",
"@tanstack/react-virtual": "^3.13.12",
"arctic": "^3.7.0",
@@ -40,16 +41,19 @@
"drizzle-orm": "^0.44.7",
"drizzle-seed": "^0.3.1",
"lucide-react": "^0.548.0",
+ "motion": "^12.23.24",
"nanoid": "^5.1.6",
"next": "15.5.6",
"next-themes": "^0.4.6",
+ "ogl": "^1.0.11",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-intersection-observer": "^9.16.0",
"react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1",
"sonner": "^2.0.7",
- "tailwind-merge": "^3.3.1",
+ "svg-dotted-map": "^2.0.1",
+ "tailwind-merge": "^3.4.0",
"tailwindcss-animate": "^1.0.7",
"vaul": "^1.1.2",
"zod": "^4.1.12"
@@ -57,14 +61,14 @@
"devDependencies": {
"@biomejs/biome": "2.3.1",
"@faker-js/faker": "^10.1.0",
- "@tailwindcss/postcss": "^4.1.16",
+ "@tailwindcss/postcss": "^4.1.17",
"@tailwindcss/typography": "^0.5.19",
- "@types/node": "^22.18.12",
- "@types/react": "^19.2.2",
- "@types/react-dom": "^19.2.2",
+ "@types/node": "^22.19.1",
+ "@types/react": "^19.2.6",
+ "@types/react-dom": "^19.2.3",
"drizzle-kit": "0.31.5",
- "lefthook": "^2.0.1",
- "tailwindcss": "^4.1.16",
+ "lefthook": "^2.0.4",
+ "tailwindcss": "^4.1.17",
"typescript": "^5.9.3"
}
}