{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "og-image-explorer",
  "title": "OG Image Explorer",
  "description": "A dev tool to audit and preview Open Graph images from all routes in your app. Visual gallery with status indicators for broken images.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/blocks/media/og-image-explorer/components/elements/og-image-explorer.tsx",
      "content": "\"use client\";\n\nimport { useMemo } from \"react\";\n\nimport { ImageOff } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nimport { OgCategorySection } from \"./og-category-section\";\n\ninterface OgRoute {\n  path: string;\n  title?: string;\n  ogImageUrl: string;\n  category?: string;\n}\n\ninterface OgImageExplorerProps {\n  routes: OgRoute[];\n  maxPerCategory?: number;\n  showPath?: boolean;\n  showTitle?: boolean;\n  className?: string;\n}\n\nfunction getCategoryFromPath(path: string): string {\n  const segments = path.split(\"/\").filter(Boolean);\n  return segments[0] || \"root\";\n}\n\nexport function OgImageExplorer({\n  routes,\n  maxPerCategory = 8,\n  showPath = true,\n  showTitle = true,\n  className,\n}: OgImageExplorerProps) {\n  const groupedRoutes = useMemo(() => {\n    const groups = new Map<string, OgRoute[]>();\n\n    for (const route of routes) {\n      const category = route.category || getCategoryFromPath(route.path);\n      const existing = groups.get(category) || [];\n      groups.set(category, [...existing, route]);\n    }\n\n    const sortedCategories = Array.from(groups.entries()).sort(([a], [b]) => {\n      if (a === \"root\") return -1;\n      if (b === \"root\") return 1;\n      return a.localeCompare(b);\n    });\n\n    return sortedCategories;\n  }, [routes]);\n\n  if (routes.length === 0) {\n    return (\n      <div\n        data-slot=\"og-image-explorer\"\n        className={cn(\n          \"flex min-h-[200px] flex-col items-center justify-center gap-3 rounded-lg border border-dashed p-8 text-center\",\n          className,\n        )}\n      >\n        <ImageOff className=\"h-10 w-10 text-muted-foreground\" />\n        <div className=\"flex flex-col gap-1\">\n          <p className=\"text-sm font-medium text-foreground\">\n            No routes configured\n          </p>\n          <p className=\"text-xs text-muted-foreground\">\n            Add routes to explore your OG images\n          </p>\n        </div>\n      </div>\n    );\n  }\n\n  return (\n    <div\n      data-slot=\"og-image-explorer\"\n      className={cn(\"flex flex-col gap-8\", className)}\n    >\n      <div className=\"flex items-center justify-between border-b pb-4\">\n        <div className=\"flex flex-col gap-1\">\n          <h1 className=\"text-xl font-bold text-foreground\">\n            OG Image Explorer\n          </h1>\n          <p className=\"text-sm text-muted-foreground\">\n            {routes.length} route{routes.length !== 1 ? \"s\" : \"\"} across{\" \"}\n            {groupedRoutes.length} categor\n            {groupedRoutes.length !== 1 ? \"ies\" : \"y\"}\n          </p>\n        </div>\n      </div>\n\n      {groupedRoutes.map(([category, categoryRoutes]) => (\n        <OgCategorySection\n          key={category}\n          category={category}\n          routes={categoryRoutes}\n          maxItems={maxPerCategory}\n          showPath={showPath}\n          showTitle={showTitle}\n        />\n      ))}\n    </div>\n  );\n}\n",
      "type": "registry:component"
    },
    {
      "path": "registry/default/blocks/media/og-image-explorer/components/elements/og-category-section.tsx",
      "content": "\"use client\";\n\nimport { useState } from \"react\";\n\nimport { ChevronDown, ChevronUp } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\nimport { OgImageCard } from \"./og-image-card\";\n\ninterface OgRoute {\n  path: string;\n  title?: string;\n  ogImageUrl: string;\n  category?: string;\n}\n\ninterface OgCategorySectionProps {\n  category: string;\n  routes: OgRoute[];\n  maxItems?: number;\n  showPath?: boolean;\n  showTitle?: boolean;\n  className?: string;\n}\n\nexport function OgCategorySection({\n  category,\n  routes,\n  maxItems = 8,\n  showPath = true,\n  showTitle = true,\n  className,\n}: OgCategorySectionProps) {\n  const [expanded, setExpanded] = useState(false);\n\n  const hasMore = routes.length > maxItems;\n  const visibleRoutes = expanded ? routes : routes.slice(0, maxItems);\n  const hiddenCount = routes.length - maxItems;\n\n  const formatCategoryName = (cat: string) => {\n    if (cat === \"root\") return \"Root\";\n    return cat.charAt(0).toUpperCase() + cat.slice(1);\n  };\n\n  return (\n    <section\n      data-slot=\"og-category-section\"\n      className={cn(\"flex flex-col gap-4\", className)}\n    >\n      <div className=\"flex items-center justify-between\">\n        <div className=\"flex items-center gap-2\">\n          <h2 className=\"text-lg font-semibold text-foreground\">\n            {formatCategoryName(category)}\n          </h2>\n          <span className=\"rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground\">\n            {routes.length}\n          </span>\n        </div>\n\n        {hasMore && (\n          <button\n            type=\"button\"\n            onClick={() => setExpanded(!expanded)}\n            className={cn(\n              \"flex items-center gap-1 text-sm text-muted-foreground transition-colors\",\n              \"hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring rounded-md px-2 py-1\",\n            )}\n          >\n            {expanded ? (\n              <>\n                View less\n                <ChevronUp className=\"h-4 w-4\" />\n              </>\n            ) : (\n              <>\n                View {hiddenCount} more\n                <ChevronDown className=\"h-4 w-4\" />\n              </>\n            )}\n          </button>\n        )}\n      </div>\n\n      <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4\">\n        {visibleRoutes.map((route) => (\n          <OgImageCard\n            key={route.path}\n            path={route.path}\n            title={route.title}\n            ogImageUrl={route.ogImageUrl}\n            showPath={showPath}\n            showTitle={showTitle}\n          />\n        ))}\n      </div>\n    </section>\n  );\n}\n",
      "type": "registry:component"
    },
    {
      "path": "registry/default/blocks/media/og-image-explorer/components/elements/og-image-card.tsx",
      "content": "\"use client\";\n\nimport { useState } from \"react\";\n\nimport { ExternalLink, ImageOff } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\ntype ImageStatus = \"loading\" | \"loaded\" | \"error\";\n\ninterface OgImageCardProps {\n  path: string;\n  title?: string;\n  ogImageUrl: string;\n  showPath?: boolean;\n  showTitle?: boolean;\n  className?: string;\n}\n\nexport function OgImageCard({\n  path,\n  title,\n  ogImageUrl,\n  showPath = true,\n  showTitle = true,\n  className,\n}: OgImageCardProps) {\n  const [status, setStatus] = useState<ImageStatus>(\"loading\");\n\n  const handleClick = () => {\n    window.open(ogImageUrl, \"_blank\", \"noopener,noreferrer\");\n  };\n\n  const statusBorderColor = {\n    loading: \"border-muted-foreground/30\",\n    loaded: \"border-green-500/50\",\n    error: \"border-red-500/50\",\n  };\n\n  return (\n    <div\n      data-slot=\"og-image-card\"\n      className={cn(\"group flex flex-col gap-2\", className)}\n    >\n      <button\n        type=\"button\"\n        onClick={handleClick}\n        className={cn(\n          \"relative aspect-[1.91/1] w-full overflow-hidden rounded-lg border-2 transition-all duration-200\",\n          \"hover:scale-[1.02] hover:shadow-lg focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n          \"bg-muted\",\n          statusBorderColor[status],\n        )}\n      >\n        {status === \"loading\" && (\n          <div className=\"absolute inset-0 animate-pulse bg-muted\" />\n        )}\n\n        {status === \"error\" ? (\n          <div className=\"absolute inset-0 flex flex-col items-center justify-center gap-2 text-muted-foreground\">\n            <ImageOff className=\"h-8 w-8\" />\n            <span className=\"text-xs\">Failed to load</span>\n          </div>\n        ) : (\n          <img\n            src={ogImageUrl}\n            alt={title || path}\n            className={cn(\n              \"h-full w-full object-cover transition-opacity duration-200\",\n              status === \"loading\" ? \"opacity-0\" : \"opacity-100\",\n            )}\n            onLoad={() => setStatus(\"loaded\")}\n            onError={() => setStatus(\"error\")}\n          />\n        )}\n\n        <div className=\"absolute bottom-2 right-2 rounded-md bg-background/80 p-1.5 opacity-0 backdrop-blur-sm transition-opacity group-hover:opacity-100\">\n          <ExternalLink className=\"h-3.5 w-3.5 text-foreground\" />\n        </div>\n      </button>\n\n      <div className=\"flex flex-col gap-0.5 px-1\">\n        {showTitle && title && (\n          <span className=\"truncate text-sm font-medium text-foreground\">\n            {title}\n          </span>\n        )}\n        {showPath && (\n          <span className=\"truncate text-xs text-muted-foreground font-mono\">\n            {path}\n          </span>\n        )}\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component"
    },
    {
      "path": "registry/default/blocks/media/og-image-explorer/routes/layout.tsx",
      "content": "export default function OgImageExplorerLayout({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return (\n    <div className=\"min-h-screen bg-background p-6 md:p-10\">\n      <div className=\"mx-auto max-w-7xl\">{children}</div>\n    </div>\n  );\n}\n",
      "type": "registry:page",
      "target": "app/og-explorer/layout.tsx"
    },
    {
      "path": "registry/default/blocks/media/og-image-explorer/routes/page.tsx",
      "content": "// Auto-generated by scripts/generate-og-routes.ts\n// Run: bun run scripts/generate-og-routes.ts\n\nimport { OgImageExplorer } from \"../../components/elements/og-image-explorer\";\n\nconst BASE_URL = \"https://tryelements.dev\";\n\nconst ROUTES = [\n  // Pages (root-level)\n  { path: \"/\", title: \"Home\", category: \"pages\" },\n  { path: \"/contributors\", title: \"Contributors\", category: \"pages\" },\n  { path: \"/sponsor\", title: \"Sponsor\", category: \"pages\" },\n  // Providers\n  { path: \"/docs\", title: \"Documentation\", category: \"providers\" },\n  { path: \"/docs/clerk\", title: \"Clerk\", category: \"providers\" },\n  { path: \"/docs/devtools\", title: \"Dev Tools\", category: \"providers\" },\n  { path: \"/docs/github\", title: \"GitHub\", category: \"providers\" },\n  { path: \"/docs/logos\", title: \"Brand Logos\", category: \"providers\" },\n  { path: \"/docs/polar\", title: \"Polar\", category: \"providers\" },\n  { path: \"/docs/theme\", title: \"Theme\", category: \"providers\" },\n  { path: \"/docs/tinte\", title: \"Tinte\", category: \"providers\" },\n  { path: \"/docs/uploadthing\", title: \"UploadThing\", category: \"providers\" },\n  // Components - Dev Tools\n  {\n    path: \"/docs/devtools/og-image-explorer\",\n    title: \"OG Image Explorer\",\n    category: \"devtools\",\n  },\n  // Components - GitHub\n  {\n    path: \"/docs/github/github-contributions\",\n    title: \"GitHub Contributions\",\n    category: \"github\",\n  },\n  {\n    path: \"/docs/github/github-stars\",\n    title: \"GitHub Stars\",\n    category: \"github\",\n  },\n  // Components - Polar\n  {\n    path: \"/docs/polar/polar-sponsorship\",\n    title: \"Polar Sponsorship\",\n    category: \"polar\",\n  },\n  // Components - Theme\n  {\n    path: \"/docs/theme/theme-switcher-button\",\n    title: \"Theme Switcher Button\",\n    category: \"theme\",\n  },\n  {\n    path: \"/docs/theme/theme-switcher-classic\",\n    title: \"Theme Switcher Classic\",\n    category: \"theme\",\n  },\n  {\n    path: \"/docs/theme/theme-switcher-dropdown\",\n    title: \"Theme Switcher Dropdown\",\n    category: \"theme\",\n  },\n  {\n    path: \"/docs/theme/theme-switcher-multi-button\",\n    title: \"Theme Switcher Multi Button\",\n    category: \"theme\",\n  },\n  {\n    path: \"/docs/theme/theme-switcher-switch\",\n    title: \"Theme Switcher Switch\",\n    category: \"theme\",\n  },\n  {\n    path: \"/docs/theme/theme-switcher-toggle\",\n    title: \"Theme Switcher Toggle\",\n    category: \"theme\",\n  },\n  // Components - Tinte\n  {\n    path: \"/docs/tinte/tinte-editor\",\n    title: \"Tinte Theme Editor\",\n    category: \"tinte\",\n  },\n  // Components - UploadThing\n  {\n    path: \"/docs/uploadthing/uploadthing-button\",\n    title: \"UploadThing Button\",\n    category: \"uploadthing\",\n  },\n  {\n    path: \"/docs/uploadthing/uploadthing-dropzone\",\n    title: \"UploadThing Dropzone\",\n    category: \"uploadthing\",\n  },\n].map((route) => ({\n  ...route,\n  ogImageUrl: `${BASE_URL}${route.path}/opengraph-image`,\n}));\n\nexport default function OgImageExplorerPage() {\n  return <OgImageExplorer routes={ROUTES} maxPerCategory={8} />;\n}\n",
      "type": "registry:page",
      "target": "app/og-explorer/page.tsx"
    }
  ],
  "docs": "Configure the `routes` prop with your app's routes and OG image URLs. The component auto-groups by route category and shows green/red borders for loaded/broken images.",
  "categories": [
    "media",
    "seo",
    "dev-tools"
  ],
  "type": "registry:block"
}