{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "web-preview",
  "title": "Web Preview",
  "description": "A sandboxed frame for whatever the agent just built, with the widths to check it at.",
  "dependencies": [
    "lucide-react@^1.31.0"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/default/web-preview/web-preview.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ExternalLink, RotateCw } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport type PreviewSize = {\n  id: string\n  label: string\n  width: number\n}\n\nexport type WebPreviewProps = Omit<\n  React.HTMLAttributes<HTMLDivElement>,\n  \"children\" | \"title\"\n> & {\n  src: string\n  /**\n   * Names the frame for assistive technology. Required, because a frame with\n   * no name is announced as \"frame\" and nothing else.\n   */\n  title: string\n  sizes?: readonly PreviewSize[]\n  defaultSize?: string\n  /** Let someone type a different address. */\n  editable?: boolean\n  onNavigate?: (src: string) => void\n  /** Replaces the default sandbox. Read the note before widening it. */\n  sandbox?: string\n  height?: number\n}\n\nconst SIZES: PreviewSize[] = [\n  { id: \"phone\", label: \"Phone\", width: 390 },\n  { id: \"tablet\", label: \"Tablet\", width: 768 },\n  { id: \"full\", label: \"Full\", width: 0 },\n]\n\n/**\n * Scripts and forms, but never allow-same-origin alongside allow-scripts: a\n * frame given both can reach into its own sandbox attribute and take the rest.\n */\nconst SANDBOX = \"allow-scripts allow-forms allow-popups allow-modals\"\n\nexport function WebPreview({\n  src,\n  title,\n  sizes = SIZES,\n  defaultSize,\n  editable = false,\n  onNavigate,\n  sandbox = SANDBOX,\n  height = 420,\n  className,\n  ...rootProps\n}: WebPreviewProps) {\n  // Everything hangs off src, so a new src resets the bar without an effect\n  // reaching in to reset it.\n  const [nav, setNav] = React.useState<{\n    from: string\n    address: string\n    typed: string\n  } | null>(null)\n  const here = nav?.from === src ? nav : null\n  const address = here?.address ?? src\n  const typed = here?.typed ?? src\n  const [size, setSize] = React.useState(defaultSize ?? sizes.at(-1)?.id ?? \"\")\n  const [generation, setGeneration] = React.useState(0)\n  const [box, setBox] = React.useState(0)\n  const frameRef = React.useRef<HTMLDivElement>(null)\n  const addressId = React.useId()\n\n  React.useEffect(() => {\n    const element = frameRef.current\n    if (!element) return\n\n    const observer = new ResizeObserver(([entry]) => {\n      setBox(entry?.contentRect.width ?? 0)\n    })\n\n    observer.observe(element)\n    return () => observer.disconnect()\n  }, [])\n\n  const chosen = sizes.find((option) => option.id === size)\n  const width = chosen?.width ?? 0\n  // A narrow viewport shown inside a narrower panel is scaled down rather than\n  // clipped, so a phone width stays a phone width to the page inside it.\n  const scale = width > 0 && box > 0 ? Math.min(1, box / width) : 1\n\n  const go = (next: string) => {\n    setNav({ from: src, address: next, typed: next })\n    setGeneration((count) => count + 1)\n    onNavigate?.(next)\n  }\n\n  return (\n    <div\n      data-slot=\"web-preview\"\n      className={cn(\n        \"border-border bg-background flex w-full flex-col overflow-hidden rounded-xl border\",\n        className\n      )}\n      {...rootProps}\n    >\n      <div className=\"border-border flex items-center gap-2 border-b p-2\">\n        <button\n          type=\"button\"\n          aria-label=\"Reload the preview\"\n          onClick={() => setGeneration((count) => count + 1)}\n          className=\"text-muted-foreground hover:text-foreground focus-visible:ring-ring inline-flex size-8 shrink-0 items-center justify-center rounded-md transition-colors focus-visible:ring-2 focus-visible:outline-none motion-reduce:transition-none\"\n        >\n          <RotateCw aria-hidden=\"true\" size={14} />\n        </button>\n\n        <label className=\"sr-only\" htmlFor={addressId}>\n          Address\n        </label>\n        <input\n          id={addressId}\n          value={typed}\n          readOnly={!editable}\n          onChange={(event) =>\n            setNav({ from: src, address, typed: event.target.value })\n          }\n          onKeyDown={(event) => {\n            if (event.key === \"Enter\" && editable) go(typed)\n          }}\n          className=\"bg-muted/50 text-muted-foreground focus-visible:ring-ring min-w-0 flex-1 truncate rounded-md px-2 py-1.5 font-mono text-xs read-only:cursor-default focus-visible:ring-2 focus-visible:outline-none\"\n        />\n\n        {sizes.length > 1 ? (\n          <div\n            role=\"group\"\n            aria-label=\"Preview width\"\n            className=\"hidden shrink-0 items-center gap-0.5 sm:flex\"\n          >\n            {sizes.map((option) => (\n              <button\n                key={option.id}\n                type=\"button\"\n                aria-pressed={option.id === size}\n                onClick={() => setSize(option.id)}\n                className=\"text-muted-foreground aria-pressed:bg-muted aria-pressed:text-foreground hover:text-foreground focus-visible:ring-ring inline-flex h-8 items-center rounded-md px-2 text-xs font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none motion-reduce:transition-none\"\n              >\n                {option.label}\n              </button>\n            ))}\n          </div>\n        ) : null}\n\n        <a\n          href={address}\n          target=\"_blank\"\n          rel=\"noreferrer noopener\"\n          className=\"text-muted-foreground hover:text-foreground focus-visible:ring-ring inline-flex size-8 shrink-0 items-center justify-center rounded-md transition-colors focus-visible:ring-2 focus-visible:outline-none motion-reduce:transition-none\"\n        >\n          <ExternalLink aria-hidden=\"true\" size={14} />\n          <span className=\"sr-only\">Open {title} in a new tab</span>\n        </a>\n      </div>\n\n      <div\n        ref={frameRef}\n        className=\"bg-muted/20 flex justify-center overflow-hidden\"\n        style={{ height }}\n      >\n        <iframe\n          key={generation}\n          src={address}\n          title={title}\n          sandbox={sandbox}\n          referrerPolicy=\"no-referrer\"\n          loading=\"lazy\"\n          className=\"border-0 bg-white\"\n          style={{\n            width: width > 0 ? width : \"100%\",\n            height: width > 0 ? height / scale : \"100%\",\n            transform: scale < 1 ? `scale(${scale})` : undefined,\n            transformOrigin: \"top center\",\n          }}\n        />\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "ai",
    "agent",
    "code",
    "preview",
    "iframe"
  ],
  "type": "registry:ui"
}