diff --git a/package.json b/package.json
index 019aa26..9b221bc 100644
--- a/package.json
+++ b/package.json
@@ -19,9 +19,11 @@
"clsx": "^2.1.1",
"lucide-react": "^0.548.0",
"nanoid": "^5.1.6",
+ "next-themes": "^0.4.6",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-router-dom": "^7.9.4",
+ "sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.16",
"uuid": "^13.0.0"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index bca26d2..b195b49 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -38,6 +38,9 @@ importers:
nanoid:
specifier: ^5.1.6
version: 5.1.6
+ next-themes:
+ specifier: ^0.4.6
+ version: 0.4.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
react:
specifier: ^19.1.1
version: 19.2.0
@@ -47,6 +50,9 @@ importers:
react-router-dom:
specifier: ^7.9.4
version: 7.9.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ sonner:
+ specifier: ^2.0.7
+ version: 2.0.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
tailwind-merge:
specifier: ^3.3.1
version: 3.3.1
@@ -1280,6 +1286,12 @@ packages:
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
+ next-themes@0.4.6:
+ resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==}
+ peerDependencies:
+ react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
+
node-releases@2.0.26:
resolution: {integrity: sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==}
@@ -1472,6 +1484,12 @@ packages:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
+ sonner@2.0.7:
+ resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==}
+ peerDependencies:
+ react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
@@ -2702,6 +2720,11 @@ snapshots:
natural-compare@1.4.0: {}
+ next-themes@0.4.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
+ dependencies:
+ react: 19.2.0
+ react-dom: 19.2.0(react@19.2.0)
+
node-releases@2.0.26: {}
optionator@0.9.4:
@@ -2854,6 +2877,11 @@ snapshots:
shebang-regex@3.0.0: {}
+ sonner@2.0.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
+ dependencies:
+ react: 19.2.0
+ react-dom: 19.2.0(react@19.2.0)
+
source-map-js@1.2.1: {}
strip-json-comments@3.1.1: {}
diff --git a/src/components/tool/index.tsx b/src/components/tool/index.tsx
index 64aa641..44e56ac 100644
--- a/src/components/tool/index.tsx
+++ b/src/components/tool/index.tsx
@@ -1,7 +1,8 @@
import type { ReactNode } from 'react';
-import { Hash } from 'lucide-react'
+import { FileJson, Hash } from 'lucide-react'
import UUID from './uuid'
+import JSON from './json'
export interface Tool {
path: string;
@@ -18,5 +19,12 @@ export const tools: Tool[] = [
description: "Generate a UUID",
icon: ,
component: ,
+ },
+ {
+ path: "json",
+ name: "JSON Formatter",
+ description: "Format and validate JSON",
+ icon: ,
+ component: ,
}
];
\ No newline at end of file
diff --git a/src/components/tool/json.tsx b/src/components/tool/json.tsx
new file mode 100644
index 0000000..d7760a5
--- /dev/null
+++ b/src/components/tool/json.tsx
@@ -0,0 +1,49 @@
+import { useState, type FC } from "react";
+import { Textarea } from "@/components/ui/textarea";
+import { Button } from "@/components/ui/button";
+import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
+import { toast } from "sonner";
+
+const Tool: FC = () => {
+ const [json, setJson] = useState("");
+
+ const validateJson = () => {
+ try {
+ JSON.parse(json);
+ return true;
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ toast.error(error.message);
+ } else {
+ toast.error("Invalid JSON");
+ }
+ return false;
+ }
+ };
+
+ const minifyJson = () => {
+ if (!validateJson()) return;
+ const formattedJson = JSON.stringify(JSON.parse(json), null, 0);
+ setJson(formattedJson);
+ toast.success("Minified successfully");
+ };
+
+ const prettifyJson = () => {
+ if (!validateJson()) return;
+ const formattedJson = JSON.stringify(JSON.parse(json), null, 2);
+ setJson(formattedJson);
+ toast.success("JSON prettified successfully");
+ };
+
+ return (
+
+ );
+};
+
+export default Tool;
diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx
new file mode 100644
index 0000000..1421354
--- /dev/null
+++ b/src/components/ui/alert.tsx
@@ -0,0 +1,66 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const alertVariants = cva(
+ "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
+ {
+ variants: {
+ variant: {
+ default: "bg-card text-card-foreground",
+ destructive:
+ "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+function Alert({
+ className,
+ variant,
+ ...props
+}: React.ComponentProps<"div"> & VariantProps) {
+ return (
+
+ )
+}
+
+function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function AlertDescription({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+export { Alert, AlertTitle, AlertDescription }
diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx
new file mode 100644
index 0000000..9f46e06
--- /dev/null
+++ b/src/components/ui/sonner.tsx
@@ -0,0 +1,38 @@
+import {
+ CircleCheckIcon,
+ InfoIcon,
+ Loader2Icon,
+ OctagonXIcon,
+ TriangleAlertIcon,
+} from "lucide-react"
+import { useTheme } from "next-themes"
+import { Toaster as Sonner, type ToasterProps } from "sonner"
+
+const Toaster = ({ ...props }: ToasterProps) => {
+ const { theme = "system" } = useTheme()
+
+ return (
+ ,
+ info: ,
+ warning: ,
+ error: ,
+ loading: ,
+ }}
+ style={
+ {
+ "--normal-bg": "var(--popover)",
+ "--normal-text": "var(--popover-foreground)",
+ "--normal-border": "var(--border)",
+ "--border-radius": "var(--radius)",
+ } as React.CSSProperties
+ }
+ {...props}
+ />
+ )
+}
+
+export { Toaster }
diff --git a/src/components/ui/textarea.tsx b/src/components/ui/textarea.tsx
new file mode 100644
index 0000000..7f21b5e
--- /dev/null
+++ b/src/components/ui/textarea.tsx
@@ -0,0 +1,18 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
+ return (
+
+ )
+}
+
+export { Textarea }
diff --git a/src/main.tsx b/src/main.tsx
index 72ea416..b5a9d99 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,10 +1,14 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
+
+import { Toaster } from '@/components/ui/sonner'
+
import './index.css'
import { AppRouter } from './router'
createRoot(document.getElementById('root')!).render(
+
)