diff --git a/packages/website/app/inspect/layout.tsx b/packages/website/app/inspect/layout.tsx
new file mode 100644
index 000000000..44aae8fac
--- /dev/null
+++ b/packages/website/app/inspect/layout.tsx
@@ -0,0 +1,48 @@
+import type { Metadata } from "next";
+
+const title = "Inspect";
+const description =
+ "Paste React Grab output to visualize element context in raw and formatted views.";
+const ogImageUrl = `https://react-grab.com/api/og?title=${encodeURIComponent(title)}&subtitle=${encodeURIComponent(description)}`;
+
+export const metadata: Metadata = {
+ title: `${title} | React Grab`,
+ description,
+ openGraph: {
+ title: `${title} | React Grab`,
+ description,
+ url: "https://react-grab.com/inspect",
+ siteName: "React Grab",
+ images: [
+ {
+ url: ogImageUrl,
+ width: 1200,
+ height: 630,
+ alt: `React Grab - ${title}`,
+ },
+ ],
+ locale: "en_US",
+ type: "website",
+ },
+ twitter: {
+ card: "summary_large_image",
+ title: `${title} | React Grab`,
+ description,
+ images: [ogImageUrl],
+ },
+ alternates: {
+ canonical: "https://react-grab.com/inspect",
+ },
+};
+
+interface InspectLayoutProps {
+ children: React.ReactNode;
+}
+
+const InspectLayout = (props: InspectLayoutProps) => {
+ return props.children;
+};
+
+InspectLayout.displayName = "InspectLayout";
+
+export default InspectLayout;
diff --git a/packages/website/app/inspect/page.tsx b/packages/website/app/inspect/page.tsx
new file mode 100644
index 000000000..db5f63eba
--- /dev/null
+++ b/packages/website/app/inspect/page.tsx
@@ -0,0 +1,476 @@
+"use client";
+
+import { useState, useEffect, useCallback } from "react";
+import { ReactGrabLogo } from "@/components/react-grab-logo";
+import { cn } from "@/utils/cn";
+import { highlightCode } from "@/lib/shiki";
+import { ArrowLeft, Clipboard, X } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import Link from "next/link";
+import prettyMs from "pretty-ms";
+
+const REACT_GRAB_MIME_TYPE = "application/x-react-grab";
+const STACK_FRAME_PATTERN =
+ /^\s+in\s+(\S+?)(?:\s+\(at\s+([^:)]+?)(?::(\d+))?(?::(\d+))?\))?$/;
+
+const TABS = ["raw", "formatted"] as const;
+type Tab = (typeof TABS)[number];
+
+interface ReactGrabEntry {
+ tagName?: string;
+ componentName?: string;
+ content: string;
+ commentText?: string;
+}
+
+interface ReactGrabMetadata {
+ version: string;
+ content: string;
+ entries: ReactGrabEntry[];
+ timestamp: number;
+}
+
+interface ParsedStackFrame {
+ componentName: string;
+ filePath: string | null;
+ lineNumber: number | null;
+ columnNumber: number | null;
+}
+
+interface ParsedContent {
+ htmlSnippet: string;
+ stackFrames: ParsedStackFrame[];
+}
+
+const parseReactGrabMetadata = (
+ jsonString: string,
+): ReactGrabMetadata | null => {
+ try {
+ const parsed = JSON.parse(jsonString);
+ if (
+ parsed &&
+ typeof parsed.content === "string" &&
+ Array.isArray(parsed.entries)
+ ) {
+ return parsed;
+ }
+ return null;
+ } catch {
+ return null;
+ }
+};
+
+const parseEntryContent = (content: string): ParsedContent => {
+ const lines = content.split("\n");
+ const snippetLines: string[] = [];
+ const stackFrames: ParsedStackFrame[] = [];
+
+ for (const line of lines) {
+ const match = STACK_FRAME_PATTERN.exec(line);
+ if (match) {
+ stackFrames.push({
+ componentName: match[1],
+ filePath: match[2] ?? null,
+ lineNumber: match[3] ? Number(match[3]) : null,
+ columnNumber: match[4] ? Number(match[4]) : null,
+ });
+ } else {
+ snippetLines.push(line);
+ }
+ }
+
+ return {
+ htmlSnippet: snippetLines.join("\n").trimEnd(),
+ stackFrames,
+ };
+};
+
+const formatTimestamp = (timestamp: number): string => {
+ const elapsed = Date.now() - timestamp;
+ if (elapsed < 1000) return "just now";
+ return `${prettyMs(elapsed, { compact: true })} ago`;
+};
+
+const useIsMac = (): boolean => {
+ const [isMac, setIsMac] = useState(false);
+
+ useEffect(() => {
+ setIsMac(navigator.platform.toUpperCase().includes("MAC"));
+ }, []);
+
+ return isMac;
+};
+
+const ElementTag = (props: { children: string }) => (
+
+ {props.children}
+
+);
+
+ElementTag.displayName = "ElementTag";
+
+const InfoRow = (props: {
+ label: string;
+ children: React.ReactNode;
+ borderTop?: boolean;
+}) => (
+
+
+ {props.label}
+
+ {props.children}
+
+);
+
+InfoRow.displayName = "InfoRow";
+
+interface FormattedEntryCardProps {
+ entry: ReactGrabEntry;
+ highlightedSnippet: string;
+ parsedContent: ParsedContent;
+ entryIndex: number;
+ totalEntries: number;
+}
+
+const FormattedEntryCard = (props: FormattedEntryCardProps) => {
+ const hasStack = props.parsedContent.stackFrames.length > 0;
+ const firstFrame = hasStack ? props.parsedContent.stackFrames[0] : null;
+
+ return (
+
+ {props.totalEntries > 1 && (
+
+ Entry {props.entryIndex + 1} of {props.totalEntries}
+
+ )}
+
+ {props.entry.tagName && (
+
+ {`<${props.entry.tagName}>`}
+
+ )}
+
+ {props.entry.componentName && (
+
+
+ {props.entry.componentName}
+
+
+ )}
+
+ {firstFrame?.filePath && (
+
+
+ {firstFrame.filePath}
+ {firstFrame.lineNumber !== null && (
+
+ :{firstFrame.lineNumber}
+ {firstFrame.columnNumber !== null &&
+ `:${firstFrame.columnNumber}`}
+
+ )}
+
+
+ )}
+
+ {props.entry.commentText && (
+
+
+ “{props.entry.commentText}”
+
+
+ )}
+
+ {props.parsedContent.htmlSnippet && (
+
+ )}
+
+ {hasStack && (
+
+
+ Stack
+
+
+ {props.parsedContent.stackFrames.map((frame, frameIndex) => (
+
+
+ {frame.componentName}
+
+ {frame.filePath && (
+
+ {frame.filePath}
+ {frame.lineNumber !== null && `:${frame.lineNumber}`}
+ {frame.columnNumber !== null && `:${frame.columnNumber}`}
+
+ )}
+
+ ))}
+
+
+ )}
+
+ );
+};
+
+FormattedEntryCard.displayName = "FormattedEntryCard";
+
+interface HighlightedEntryData {
+ entry: ReactGrabEntry;
+ parsedContent: ParsedContent;
+ highlightedSnippet: string;
+}
+
+const InspectPage = () => {
+ const [pastedContent, setPastedContent] = useState(null);
+ const [metadata, setMetadata] = useState(null);
+ const [activeTab, setActiveTab] = useState("raw");
+ const [highlightedFullContent, setHighlightedFullContent] = useState<
+ string | null
+ >(null);
+ const [highlightedEntryData, setHighlightedEntryData] = useState<
+ HighlightedEntryData[]
+ >([]);
+ const isMac = useIsMac();
+
+ const handleClear = useCallback(() => {
+ setPastedContent(null);
+ setMetadata(null);
+ setHighlightedFullContent(null);
+ setHighlightedEntryData([]);
+ setActiveTab("raw");
+ }, []);
+
+ useEffect(() => {
+ const handlePaste = (event: ClipboardEvent) => {
+ const clipboardData = event.clipboardData;
+ if (!clipboardData) return;
+
+ const reactGrabData = clipboardData.getData(REACT_GRAB_MIME_TYPE);
+ const plainText = clipboardData.getData("text/plain");
+
+ if (!reactGrabData && !plainText) return;
+
+ event.preventDefault();
+
+ const parsedMetadata = reactGrabData
+ ? parseReactGrabMetadata(reactGrabData)
+ : null;
+
+ const content = plainText || parsedMetadata?.content || "";
+ setPastedContent(content);
+ setMetadata(parsedMetadata);
+ setActiveTab(parsedMetadata ? "formatted" : "raw");
+ };
+
+ document.addEventListener("paste", handlePaste);
+ return () => document.removeEventListener("paste", handlePaste);
+ }, []);
+
+ useEffect(() => {
+ if (!pastedContent) {
+ setHighlightedFullContent(null);
+ setHighlightedEntryData([]);
+ return;
+ }
+
+ const highlightAsync = async () => {
+ const fullHtml = await highlightCode({
+ code: pastedContent,
+ lang: "html",
+ });
+ setHighlightedFullContent(fullHtml);
+
+ if (metadata?.entries.length) {
+ const results = await Promise.all(
+ metadata.entries.map(async (entry) => {
+ const parsedContent = parseEntryContent(entry.content);
+ const highlightedSnippet = parsedContent.htmlSnippet
+ ? await highlightCode({
+ code: parsedContent.htmlSnippet,
+ lang: "html",
+ })
+ : "";
+ return { entry, parsedContent, highlightedSnippet };
+ }),
+ );
+ setHighlightedEntryData(results);
+ }
+ };
+
+ highlightAsync();
+ }, [metadata, pastedContent]);
+
+ const hasContent = Boolean(pastedContent);
+
+ if (!hasContent) {
+ return (
+
+
+
+
+
+
+
+
Inspect
+
+ Paste React Grab output anywhere on this page
+
+
+
+
+
+
+ {isMac ? "⌘" : "Ctrl"} + V
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ Back to home
+
+
+
+
+
+
+
+
+
+
+ {TABS.map((tab) => (
+
+ ))}
+
+
+ {metadata && (
+
+ {metadata.version && (
+
+ v{metadata.version}
+
+ )}
+ {metadata.timestamp > 0 && (
+ {formatTimestamp(metadata.timestamp)}
+ )}
+
+ )}
+
+
+
+
+
+
+ {activeTab === "raw" && (
+
+ {highlightedFullContent ? (
+
+ ) : (
+
+ {pastedContent}
+
+ )}
+
+ )}
+
+ {activeTab === "formatted" && (
+
+ {highlightedEntryData.length > 0 ? (
+ highlightedEntryData.map((entryData, index) => (
+
+ ))
+ ) : metadata ? (
+
+ Loading…
+
+ ) : (
+
+
+ Paste React Grab output (with{" "}
+
+ application/x-react-grab
+ {" "}
+ metadata) for the structured view.
+
+
+
+ )}
+
+ )}
+
+
+ );
+};
+
+InspectPage.displayName = "InspectPage";
+
+export default InspectPage;