{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chatgpt-app",
  "type": "registry:block",
  "title": "ChatGPT Widget Frontend",
  "description": "Frontend widget implementation, build script, types, and hooks for embedding a ChatGPT MCP widget.",
  "dependencies": [
    "@tailwindcss/vite",
    "@vitejs/plugin-react",
    "fast-glob",
    "react",
    "react-dom",
    "vite"
  ],
  "registryDependencies": [
    "card",
    "skeleton"
  ],
  "files": [
    {
      "path": "registry/default/chatgpt-app/index.jsx",
      "content": "import { createRoot } from 'react-dom/client';\nimport { Card } from '@/components/ui/card';\nimport { Skeleton } from '@/components/ui/skeleton';\nimport { useWidgetProps } from '@/registry/default/chatgpt-app/use-widget-props';\n\nfunction App() {\n  const { a, b, sum } = useWidgetProps({\n    a: null,\n    b: null,\n    sum: null,\n  });\n\n  const hasData = a !== null && b !== null && sum !== null;\n\n  return (\n    <div className=\"flex items-center justify-center w-full min-h-screen\">\n      <Card className=\"w-full max-w-md p-6\">\n        <div className=\"space-y-4\">\n          <h2 className=\"text-xl font-bold\">Sum Calculator</h2>\n\n          <div className=\"space-y-2\">\n            <div className=\"flex justify-between\">\n              <span>A:</span>\n              {hasData ? <span>{a}</span> : <Skeleton className=\"h-5 w-20\" />}\n            </div>\n\n            <div className=\"flex justify-between\">\n              <span>B:</span>\n              {hasData ? <span>{b}</span> : <Skeleton className=\"h-5 w-20\" />}\n            </div>\n\n            <div className=\"flex justify-between font-bold\">\n              <span>Sum:</span>\n              {hasData ? <span>{sum}</span> : <Skeleton className=\"h-5 w-20\" />}\n            </div>\n          </div>\n        </div>\n      </Card>\n    </div>\n  );\n}\n\ncreateRoot(document.getElementById('add-root')).render(<App />);\n",
      "type": "registry:component",
      "target": "src/components/add/index.jsx"
    },
    {
      "path": "registry/default/chatgpt-app/build-chatgpt-widgets.ts",
      "content": "import crypto from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport tailwindcss from '@tailwindcss/vite';\nimport react from '@vitejs/plugin-react';\nimport fg from 'fast-glob';\nimport { build, type InlineConfig, type Plugin } from 'vite';\nimport pkg from '../../../package.json' with { type: 'json' }; // <-- TODO: Get to the package.json of your project\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst projectRoot = path.resolve(__dirname, '..', '..'); // <-- TODO: Get to where you want your built `assets` directory to appear\n\nconst entries = fg.sync('src/**/index.{tsx,jsx}', {\n  cwd: projectRoot,\n  absolute: true,\n});\nconst outDir = path.join(projectRoot, 'assets');\n\nconst PER_ENTRY_CSS_GLOB = '**/*.{css,pcss,scss,sass}';\nconst PER_ENTRY_CSS_IGNORE = '**/*.module.*'.split(',').map((s) => s.trim());\nconst GLOBAL_CSS_LIST = [path.join(projectRoot, 'src/index.css')];\n\nconst targets: string[] = ['add'];\nconst builtNames: string[] = [];\n\nexport function encryptPackageVersion(version: string): string {\n  return crypto\n    .createHash('sha256')\n    .update(version, 'utf8')\n    .digest('hex')\n    .slice(0, 4);\n}\n\nfunction wrapEntryPlugin(\n  virtualId: string,\n  entryFile: string,\n  cssPaths: string[],\n): Plugin {\n  return {\n    name: `virtual-entry-wrapper:${entryFile}`,\n    resolveId(id) {\n      if (id === virtualId) return id;\n    },\n    load(id) {\n      if (id !== virtualId) {\n        return null;\n      }\n\n      const cssImports = cssPaths\n        .map((css) => `import ${JSON.stringify(css)};`)\n        .join('\\n');\n\n      return `\n    ${cssImports}\n    export * from ${JSON.stringify(entryFile)};\n\n    import * as __entry from ${JSON.stringify(entryFile)};\n    export default (__entry.default ?? __entry.App);\n\n    import ${JSON.stringify(entryFile)};\n  `;\n    },\n  };\n}\n\nfs.rmSync(outDir, { recursive: true, force: true });\nfs.mkdirSync(outDir, { recursive: true });\n\nfor (const file of entries) {\n  const name = path.basename(path.dirname(file));\n  if (targets.length && !targets.includes(name)) {\n    continue;\n  }\n\n  const entryAbs = path.resolve(file);\n  const entryDir = path.dirname(entryAbs);\n\n  // Collect CSS for this entry using the glob(s) rooted at its directory\n  const perEntryCss = fg.sync(PER_ENTRY_CSS_GLOB, {\n    cwd: entryDir,\n    absolute: true,\n    dot: false,\n    ignore: PER_ENTRY_CSS_IGNORE,\n  });\n\n  // Global CSS (Tailwind, etc.), only include those that exist\n  const globalCss = GLOBAL_CSS_LIST.filter((p) => fs.existsSync(p));\n\n  // Final CSS list (global first for predictable cascade)\n  const cssToInclude = [...globalCss, ...perEntryCss].filter((p) =>\n    fs.existsSync(p),\n  );\n\n  const virtualId = `\\0virtual-entry:${entryAbs}`;\n\n  const createConfig = (): InlineConfig => ({\n    root: projectRoot,\n    plugins: [\n      wrapEntryPlugin(virtualId, entryAbs, cssToInclude),\n      tailwindcss(),\n      react(),\n      {\n        name: 'remove-manual-chunks',\n        outputOptions(options) {\n          if ('manualChunks' in options) {\n            delete (options as any).manualChunks;\n          }\n          return options;\n        },\n      },\n    ],\n    esbuild: {\n      jsx: 'automatic',\n      jsxImportSource: 'react',\n      target: 'es2022',\n    },\n    build: {\n      target: 'es2022',\n      outDir,\n      emptyOutDir: false,\n      chunkSizeWarningLimit: 2000,\n      minify: 'esbuild',\n      cssCodeSplit: false,\n      rollupOptions: {\n        input: virtualId,\n        output: {\n          format: 'es',\n          entryFileNames: `${name}.js`,\n          inlineDynamicImports: true,\n          assetFileNames: (info) =>\n            (info.name || '').endsWith('.css')\n              ? `${name}.css`\n              : '[name]-[hash][extname]',\n        },\n        preserveEntrySignatures: 'allow-extension',\n        treeshake: true,\n      },\n    },\n  });\n\n  console.group(`Building ${name} (react)`);\n  await build(createConfig());\n  console.groupEnd();\n  builtNames.push(name);\n  console.log(`Built ${name}`);\n}\n\nconst outputs = fs.existsSync(outDir)\n  ? fs\n      .readdirSync(outDir)\n      .filter((f) => f.endsWith('.js') || f.endsWith('.css'))\n      .map((f) => path.join(outDir, f))\n      .filter((p) => fs.existsSync(p))\n  : [];\n\nconst renamed = [];\n\nconst h = encryptPackageVersion(pkg.version);\n\nconsole.group('Hashing outputs');\nfor (const out of outputs) {\n  const dir = path.dirname(out);\n  const ext = path.extname(out);\n  const base = path.basename(out, ext);\n  const newName = path.join(dir, `${base}-${h}${ext}`);\n\n  fs.renameSync(out, newName);\n  renamed.push({ old: out, neu: newName });\n  console.log(`${out} -> ${newName}`);\n}\nconsole.groupEnd();\n\nconsole.log('new hash: ', h);\n\nfor (const name of builtNames) {\n  const dir = outDir;\n  const htmlPath = path.join(dir, `${name}-${h}.html`);\n  const cssPath = path.join(dir, `${name}-${h}.css`);\n  const jsPath = path.join(dir, `${name}-${h}.js`);\n\n  const css = fs.existsSync(cssPath)\n    ? fs.readFileSync(cssPath, { encoding: 'utf8' })\n    : '';\n  const js = fs.existsSync(jsPath)\n    ? fs.readFileSync(jsPath, { encoding: 'utf8' })\n    : '';\n\n  const cssBlock = css ? `\\n  <style>\\n${css}\\n  </style>\\n` : '';\n  const jsBlock = js ? `\\n  <script type=\"module\">\\n${js}\\n  </script>` : '';\n\n  const html = [\n    '<!doctype html>',\n    '<html>',\n    `<head>${cssBlock}</head>`,\n    '<body>',\n    `  <div id=\"${name}-root\"></div>${jsBlock}`,\n    '</body>',\n    '</html>',\n  ].join('\\n');\n  fs.writeFileSync(htmlPath, html, { encoding: 'utf8' });\n  console.log(`${htmlPath} (generated)`);\n}\n",
      "type": "registry:lib"
    },
    {
      "path": "registry/default/chatgpt-app/chatgpt-types.ts",
      "content": "export type OpenAiGlobals<\n  ToolInput = UnknownObject,\n  ToolOutput = UnknownObject,\n  ToolResponseMetadata = UnknownObject,\n  WidgetState = UnknownObject,\n> = {\n  // visuals\n  theme: Theme;\n\n  userAgent: UserAgent;\n  locale: string;\n\n  // layout\n  maxHeight: number;\n  displayMode: DisplayMode;\n  safeArea: SafeArea;\n\n  // state\n  toolInput: ToolInput;\n  toolOutput: ToolOutput | null;\n  toolResponseMetadata: ToolResponseMetadata | null;\n  widgetState: WidgetState | null;\n  setWidgetState: (state: WidgetState) => Promise<void>;\n};\n\n// currently copied from types.ts in chatgpt/web-sandbox.\n// Will eventually use a public package.\ntype API = {\n  callTool: CallTool;\n  sendFollowUpMessage: (args: { prompt: string }) => Promise<void>;\n  openExternal(payload: { href: string }): void;\n\n  // Layout controls\n  requestDisplayMode: RequestDisplayMode;\n};\n\nexport type UnknownObject = Record<string, unknown>;\n\nexport type Theme = 'light' | 'dark';\n\nexport type SafeAreaInsets = {\n  top: number;\n  bottom: number;\n  left: number;\n  right: number;\n};\n\nexport type SafeArea = {\n  insets: SafeAreaInsets;\n};\n\nexport type DeviceType = 'mobile' | 'tablet' | 'desktop' | 'unknown';\n\nexport type UserAgent = {\n  device: { type: DeviceType };\n  capabilities: {\n    hover: boolean;\n    touch: boolean;\n  };\n};\n\n/** Display mode */\nexport type DisplayMode = 'pip' | 'inline' | 'fullscreen';\nexport type RequestDisplayMode = (args: { mode: DisplayMode }) => Promise<{\n  /**\n   * The granted display mode. The host may reject the request.\n   * For mobile, PiP is always coerced to fullscreen.\n   */\n  mode: DisplayMode;\n}>;\n\nexport type CallToolResponse = {\n  result: string;\n};\n\n/** Calling APIs */\nexport type CallTool = (\n  name: string,\n  args: Record<string, unknown>,\n) => Promise<CallToolResponse>;\n\n/** Extra events */\nexport const SET_GLOBALS_EVENT_TYPE = 'openai:set_globals';\nexport class SetGlobalsEvent extends CustomEvent<{\n  globals: Partial<OpenAiGlobals>;\n}> {\n  readonly type = SET_GLOBALS_EVENT_TYPE;\n}\n\n/**\n * Global oai object injected by the web sandbox for communicating with chatgpt host page.\n */\ndeclare global {\n  interface Window {\n    openai: API & OpenAiGlobals;\n  }\n\n  interface WindowEventMap {\n    [SET_GLOBALS_EVENT_TYPE]: SetGlobalsEvent;\n  }\n}\n",
      "type": "registry:lib"
    },
    {
      "path": "registry/default/chatgpt-app/media-queries.ts",
      "content": "function matchMediaQuery(query: string) {\n  return window.matchMedia(query).matches;\n}\n\nfunction createMediaQueryFn(query: string) {\n  return () => matchMediaQuery(query);\n}\n\nexport const prefersReducedMotion = createMediaQueryFn(\n  '(prefers-reduced-motion: reduce)',\n);\n\nexport const isPrimarilyTouchDevice = createMediaQueryFn('(pointer: coarse)');\n\nexport const isHoverAvailable = createMediaQueryFn('(hover: hover)');\n",
      "type": "registry:lib"
    },
    {
      "path": "registry/default/chatgpt-app/use-openai-global.ts",
      "content": "import { useSyncExternalStore } from 'react';\nimport {\n  type OpenAiGlobals,\n  SET_GLOBALS_EVENT_TYPE,\n  type SetGlobalsEvent,\n} from '@/registry/default/chatgpt-app/chatgpt-types';\n\nexport function useOpenAiGlobal<K extends keyof OpenAiGlobals>(\n  key: K,\n): OpenAiGlobals[K] | null {\n  return useSyncExternalStore(\n    (onChange) => {\n      if (typeof window === 'undefined') {\n        return () => {};\n      }\n\n      const handleSetGlobal = (event: SetGlobalsEvent) => {\n        const value = event.detail.globals[key];\n        if (value === undefined) {\n          return;\n        }\n\n        onChange();\n      };\n\n      window.addEventListener(SET_GLOBALS_EVENT_TYPE, handleSetGlobal, {\n        passive: true,\n      });\n\n      return () => {\n        window.removeEventListener(SET_GLOBALS_EVENT_TYPE, handleSetGlobal);\n      };\n    },\n    () => window.openai?.[key] ?? null,\n    () => window.openai?.[key] ?? null,\n  );\n}\n",
      "type": "registry:hook"
    },
    {
      "path": "registry/default/chatgpt-app/use-display-mode.ts",
      "content": "import type { DisplayMode } from '@/registry/default/chatgpt-app/chatgpt-types';\nimport { useOpenAiGlobal } from '@/registry/default/chatgpt-app/use-openai-global';\n\nexport const useDisplayMode = (): DisplayMode | null => {\n  return useOpenAiGlobal('displayMode');\n};\n",
      "type": "registry:hook"
    },
    {
      "path": "registry/default/chatgpt-app/use-max-height.ts",
      "content": "import { useOpenAiGlobal } from '@/registry/default/chatgpt-app/use-openai-global';\n\nexport const useMaxHeight = (): number | null => {\n  return useOpenAiGlobal('maxHeight');\n};\n",
      "type": "registry:hook"
    },
    {
      "path": "registry/default/chatgpt-app/use-widget-props.ts",
      "content": "import { useOpenAiGlobal } from '@/registry/default/chatgpt-app/use-openai-global';\n\nexport function useWidgetProps<T extends Record<string, unknown>>(\n  defaultState?: T | (() => T),\n): T {\n  const props = useOpenAiGlobal('toolOutput') as T;\n\n  const fallback =\n    typeof defaultState === 'function'\n      ? (defaultState as () => T | null)()\n      : (defaultState ?? null);\n\n  return props ?? fallback;\n}\n",
      "type": "registry:hook"
    },
    {
      "path": "registry/default/chatgpt-app/use-widget-state.ts",
      "content": "import { type SetStateAction, useCallback, useEffect, useState } from 'react';\nimport type { UnknownObject } from '@/registry/default/chatgpt-app/chatgpt-types';\nimport { useOpenAiGlobal } from '@/registry/default/chatgpt-app/use-openai-global';\n\nexport function useWidgetState<T extends UnknownObject>(\n  defaultState: T | (() => T),\n): readonly [T, (state: SetStateAction<T>) => void];\nexport function useWidgetState<T extends UnknownObject>(\n  defaultState?: T | (() => T | null) | null,\n): readonly [T | null, (state: SetStateAction<T | null>) => void];\nexport function useWidgetState<T extends UnknownObject>(\n  defaultState?: T | (() => T | null) | null,\n): readonly [T | null, (state: SetStateAction<T | null>) => void] {\n  const widgetStateFromWindow = useOpenAiGlobal('widgetState') as T;\n\n  const [widgetState, _setWidgetState] = useState<T | null>(() => {\n    if (widgetStateFromWindow != null) {\n      return widgetStateFromWindow;\n    }\n\n    return typeof defaultState === 'function'\n      ? defaultState()\n      : (defaultState ?? null);\n  });\n\n  useEffect(() => {\n    _setWidgetState(widgetStateFromWindow);\n  }, [widgetStateFromWindow]);\n\n  const setWidgetState = useCallback(\n    (state: SetStateAction<T | null>) => {\n      _setWidgetState((prevState) => {\n        const newState = typeof state === 'function' ? state(prevState) : state;\n\n        if (newState != null) {\n          window.openai.setWidgetState(newState);\n        }\n\n        return newState;\n      });\n    },\n    [window.openai.setWidgetState],\n  );\n\n  return [widgetState, setWidgetState] as const;\n}\n",
      "type": "registry:hook"
    }
  ],
  "docs": "Install this block in your widget bundle to render the ChatGPT MCP example UI and bundle the widget assets. Pair it with the `chatgpt-app-backend` block to get the MCP server and tool registration script."
}