{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "google-chrome",
  "title": "Google Chrome (macOS)",
  "description": "macOS Google Chrome window mock with tab strip, omnibox, bookmarks bar, and Google new-tab preview (light/dark) in the viewport.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/new-york/blocks/google-chrome/page.tsx",
      "content": "import GoogleChrome from \"./components/google-chrome\"\n\nexport default function Page() {\n  return (\n    <div className=\"px-4 py-4 md:px-6 md:py-6\">\n      <GoogleChrome />\n    </div>\n  )\n}\n",
      "type": "registry:page",
      "target": "app/google-chrome/page.tsx"
    },
    {
      "path": "registry/new-york/blocks/google-chrome/components/google-chrome.tsx",
      "content": "\"use client\"\n\nimport { useCallback } from \"react\"\n\nimport {\n  ArrowLeft,\n  ArrowRight,\n  MoreVertical,\n  Plus,\n  RotateCw,\n  X,\n} from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nimport AppsIcon from \"./apps-icon\"\nimport BrowserViewport, { type BrowserViewportProps } from \"./browser-viewport\"\nimport ChromeViewportDefault from \"./chrome-viewport-default\"\nimport OmniboxTypingText from \"./omnibox-typing-text\"\nimport ProfileAvatar from \"./profile-avatar\"\nimport TrafficLights from \"./traffic-lights\"\nimport { useOmniboxTyping } from \"../hooks/use-omnibox-typing\"\nimport { useSimulatedPageLoad } from \"../hooks/use-simulated-page-load\"\nimport { chromeConfig } from \"../lib/config\"\n\nimport \"../styles/google-chrome.css\"\n\nconst navIconClass = \"size-[18px] shrink-0 stroke-[1.75]\"\nconst menuIconClass = \"size-[18px] shrink-0 stroke-[1.75]\"\nconst tabIconClass = \"size-3.5 shrink-0 stroke-[2]\"\n\ntype GoogleChromeProps = {\n  className?: string\n  omniboxTypingPrompts?: readonly string[]\n  /** When false, omnibox stops after typing the first prompt once. */\n  omniboxTypingLoop?: boolean\n  /** Fired when omnibox typing completes (entering hold). */\n  onOmniboxTypedComplete?: () => void\n  /** Fired when omnibox text is cleared (entering between). */\n  onOmniboxTypedCleared?: () => void\n} & Omit<BrowserViewportProps, \"className\">\n\nexport default function GoogleChrome({\n  className,\n  omniboxTypingPrompts = chromeConfig.omniboxTypingPrompts,\n  omniboxTypingLoop = true,\n  onOmniboxTypedComplete,\n  onOmniboxTypedCleared,\n  minHeight,\n  aspectRatio,\n  src,\n  alt,\n  videoSrc,\n  poster,\n  autoPlay,\n  children,\n}: GoogleChromeProps) {\n  const hasCustomViewport = children != null\n  const { phase, progress, startLoading } = useSimulatedPageLoad()\n\n  const handleTypedComplete = useCallback(() => {\n    if (!hasCustomViewport) startLoading()\n    onOmniboxTypedComplete?.()\n  }, [hasCustomViewport, startLoading, onOmniboxTypedComplete])\n\n  const { displayText, showCursor, caretSolid, isActive } = useOmniboxTyping({\n    prompts: omniboxTypingPrompts,\n    loop: omniboxTypingLoop,\n    onTypedComplete: handleTypedComplete,\n    onTypedCleared: onOmniboxTypedCleared,\n  })\n\n  return (\n    <div\n      className={cn(\n        \"gc-root mx-auto w-full max-w-6xl min-w-0 overflow-hidden rounded-md border border-(--gc-border) bg-(--gc-shell-bg) font-[system-ui,-apple-system,BlinkMacSystemFont,'Segoe_UI',Roboto,'Helvetica_Neue',Arial,sans-serif] shadow-(--gc-shadow) outline -outline-offset-1 outline-(--gc-ring)\",\n        className\n      )}\n      role=\"img\"\n      aria-label=\"Google Chrome browser window mockup\"\n    >\n      <div className=\"grid min-h-11 grid-cols-[auto_minmax(0,1fr)] items-end gap-2.5 bg-(--gc-tabstrip-bg) pt-1.5 pr-2 pl-3.5\">\n        <TrafficLights />\n        <div className=\"flex min-w-0 flex-1 items-end gap-0.5\">\n          <div className=\"grid w-[clamp(9.5rem,32vw,11.75rem)] max-w-47 min-w-38 shrink-0 grid-cols-[minmax(0,1fr)_auto] items-center gap-1.5 rounded-t-lg bg-(--gc-tab-bg) px-1.5 py-2 pl-3 text-3.25 leading-[1.2] text-(--gc-tab-fg)\">\n            <span className=\"min-w-0 truncate\">{chromeConfig.tabTitle}</span>\n            <button\n              type=\"button\"\n              className=\"mr-0.5 flex size-5 shrink-0 items-center justify-center self-center justify-self-end rounded-full border-none bg-transparent p-0 text-(--gc-tab-close) opacity-85 hover:opacity-100\"\n              tabIndex={-1}\n              aria-hidden\n            >\n              <X className={tabIconClass} />\n            </button>\n          </div>\n          <button\n            type=\"button\"\n            className=\"mb-2 flex size-5 shrink-0 items-center justify-center border-none bg-transparent p-0 text-(--gc-new-tab-btn)\"\n            tabIndex={-1}\n            aria-hidden\n          >\n            <Plus className={tabIconClass} />\n          </button>\n        </div>\n      </div>\n\n      <div className=\"box-border flex min-h-10.5 items-center gap-2 border-b border-(--gc-toolbar-border) bg-(--gc-toolbar-bg) px-3.5 py-1.5\">\n        <div className=\"flex shrink-0 items-center gap-px\">\n          <button\n            type=\"button\"\n            className=\"flex size-8 shrink-0 items-center justify-center rounded-full border-none bg-transparent p-0 text-(--gc-nav-fg)\"\n            tabIndex={-1}\n            aria-hidden\n          >\n            <ArrowLeft className={navIconClass} />\n          </button>\n          <button\n            type=\"button\"\n            className=\"flex size-8 shrink-0 cursor-default items-center justify-center rounded-full border-none bg-transparent p-0 text-(--gc-nav-disabled) disabled:text-(--gc-nav-disabled)\"\n            disabled\n            tabIndex={-1}\n            aria-hidden\n          >\n            <ArrowRight className={navIconClass} />\n          </button>\n          <button\n            type=\"button\"\n            className=\"flex size-8 shrink-0 items-center justify-center rounded-full border-none bg-transparent p-0 text-(--gc-nav-fg)\"\n            tabIndex={-1}\n            aria-hidden\n          >\n            <RotateCw className={navIconClass} />\n          </button>\n        </div>\n        <div\n          className={cn(\n            \"box-border flex min-h-[calc(0.8125rem*1.2+0.5rem)] min-w-0 flex-1 items-center rounded-full border border-transparent bg-(--gc-omnibox-bg) px-4.5 py-1 transition-[box-shadow,border-color] duration-200 ease-out motion-reduce:transition-none\",\n            isActive &&\n              \"border-transparent shadow-[0_0_0_1px_var(--gc-omnibox-focus-ring)]\"\n          )}\n        >\n          <OmniboxTypingText\n            displayText={displayText}\n            showCursor={showCursor}\n            caretSolid={caretSolid}\n          />\n        </div>\n        <div className=\"flex shrink-0 items-center gap-1.5\">\n          <ProfileAvatar />\n          <button\n            type=\"button\"\n            className=\"flex items-center justify-center border-none bg-transparent p-1 text-(--gc-menu-fg)\"\n            tabIndex={-1}\n            aria-hidden\n          >\n            <MoreVertical className={menuIconClass} />\n          </button>\n        </div>\n      </div>\n\n      <div className=\"flex items-center gap-2 border-b border-(--gc-toolbar-border) bg-(--gc-bookmarks-bg) px-3.5 py-1.5 text-3.25 text-(--gc-bookmarks-fg)\">\n        <AppsIcon />\n        <span>{chromeConfig.bookmarksAppsLabel}</span>\n      </div>\n\n      <BrowserViewport\n        minHeight={minHeight}\n        aspectRatio={aspectRatio}\n        src={src}\n        alt={alt}\n        videoSrc={videoSrc}\n        poster={poster}\n        autoPlay={autoPlay}\n      >\n        {children ?? (\n          <ChromeViewportDefault phase={phase} progress={progress} />\n        )}\n      </BrowserViewport>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "app/google-chrome/components/google-chrome.tsx"
    },
    {
      "path": "registry/new-york/blocks/google-chrome/components/browser-viewport.tsx",
      "content": "import type { CSSProperties, ReactNode } from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nimport { chromeConfig } from \"../lib/config\"\nimport GoogleHomePreview from \"./google-home-preview\"\n\nexport type BrowserViewportProps = {\n  className?: string\n  minHeight?: string | number\n  aspectRatio?: string\n  src?: string\n  alt?: string\n  videoSrc?: string\n  poster?: string\n  autoPlay?: boolean\n  children?: ReactNode\n}\n\nexport default function BrowserViewport({\n  className,\n  minHeight = chromeConfig.defaultViewportMinHeight,\n  aspectRatio,\n  src,\n  alt = \"\",\n  videoSrc,\n  poster,\n  autoPlay = true,\n  children,\n}: BrowserViewportProps) {\n  const style = {\n    ...(aspectRatio ? { aspectRatio } : {}),\n    minHeight: typeof minHeight === \"number\" ? `${minHeight}px` : minHeight,\n  } satisfies CSSProperties\n\n  return (\n    <div\n      className={cn(\n        \"relative w-full overflow-hidden bg-(--gc-viewport-bg)\",\n        className\n      )}\n      style={style}\n    >\n      {children ? (\n        <div className=\"gc-viewport-slot absolute inset-0 flex size-full overflow-hidden\">\n          {children}\n        </div>\n      ) : videoSrc ? (\n        <video\n          className=\"absolute inset-0 block size-full border-none object-cover object-center\"\n          src={videoSrc}\n          poster={poster}\n          muted\n          loop\n          playsInline\n          autoPlay={autoPlay}\n        />\n      ) : src ? (\n        // eslint-disable-next-line @next/next/no-img-element -- registry block; consumers may swap for next/image\n        <img\n          className=\"absolute inset-0 block size-full border-none object-cover object-center\"\n          src={src}\n          alt={alt}\n        />\n      ) : (\n        <GoogleHomePreview className=\"absolute inset-0\" />\n      )}\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "app/google-chrome/components/browser-viewport.tsx"
    },
    {
      "path": "registry/new-york/blocks/google-chrome/components/chrome-viewport-default.tsx",
      "content": "import GoogleHomePreview from \"./google-home-preview\"\nimport PageLoadingBar from \"./page-loading-bar\"\nimport type { PageLoadPhase } from \"../hooks/use-simulated-page-load\"\n\ntype ChromeViewportDefaultProps = {\n  progress: number\n  phase: PageLoadPhase\n}\n\nexport default function ChromeViewportDefault({\n  progress,\n  phase,\n}: ChromeViewportDefaultProps) {\n  return (\n    <div className=\"relative size-full\">\n      <PageLoadingBar progress={progress} visible={phase === \"loading\"} />\n      <GoogleHomePreview className=\"absolute inset-0\" />\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "app/google-chrome/components/chrome-viewport-default.tsx"
    },
    {
      "path": "registry/new-york/blocks/google-chrome/components/page-loading-bar.tsx",
      "content": "type PageLoadingBarProps = {\n  progress: number\n  visible: boolean\n}\n\nexport default function PageLoadingBar({ progress, visible }: PageLoadingBarProps) {\n  if (!visible) return null\n\n  return (\n    <div\n      className=\"pointer-events-none absolute inset-x-0 top-0 z-10 h-px overflow-hidden\"\n      aria-hidden\n    >\n      <div\n        className=\"h-full bg-[#1a73e8] dark:bg-[#8ab4f8]\"\n        style={{ width: `${Math.min(100, progress)}%` }}\n      />\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "app/google-chrome/components/page-loading-bar.tsx"
    },
    {
      "path": "registry/new-york/blocks/google-chrome/hooks/use-simulated-page-load.ts",
      "content": "\"use client\"\n\nimport { useCallback, useEffect, useRef, useState, useSyncExternalStore } from \"react\"\n\nexport type PageLoadPhase = \"idle\" | \"loading\" | \"loaded\"\n\nfunction subscribeReducedMotion(onStoreChange: () => void) {\n  const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\")\n  mq.addEventListener(\"change\", onStoreChange)\n  return () => mq.removeEventListener(\"change\", onStoreChange)\n}\n\nfunction getReducedMotion() {\n  return window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n}\n\nfunction getReducedMotionServer() {\n  return false\n}\n\n/** Chrome-like load curve: fast start, slow crawl, quick finish. */\nfunction chromeLoadProgress(elapsedMs: number): number {\n  if (elapsedMs < 350) {\n    return (elapsedMs / 350) * 68\n  }\n  if (elapsedMs < 1400) {\n    const t = (elapsedMs - 350) / 1050\n    return 68 + t * 22\n  }\n  if (elapsedMs < 1650) {\n    const t = (elapsedMs - 1400) / 250\n    return 90 + t * 10\n  }\n  return 100\n}\n\nexport function useSimulatedPageLoad() {\n  const reducedMotion = useSyncExternalStore(\n    subscribeReducedMotion,\n    getReducedMotion,\n    getReducedMotionServer,\n  )\n  const [phase, setPhase] = useState<PageLoadPhase>(\"idle\")\n  const [progress, setProgress] = useState(0)\n  const rafRef = useRef<number>(0)\n  const startRef = useRef<number>(0)\n\n  const reset = useCallback(() => {\n    cancelAnimationFrame(rafRef.current)\n    setPhase(\"idle\")\n    setProgress(0)\n  }, [])\n\n  const startLoading = useCallback(() => {\n    cancelAnimationFrame(rafRef.current)\n    if (reducedMotion) {\n      setPhase(\"loaded\")\n      setProgress(100)\n      return\n    }\n    setPhase(\"loading\")\n    setProgress(0)\n    startRef.current = performance.now()\n\n    const tick = (now: number) => {\n      const elapsed = now - startRef.current\n      const next = chromeLoadProgress(elapsed)\n      setProgress(next)\n      if (next >= 100) {\n        setPhase(\"loaded\")\n        return\n      }\n      rafRef.current = requestAnimationFrame(tick)\n    }\n    rafRef.current = requestAnimationFrame(tick)\n  }, [reducedMotion])\n\n  useEffect(() => () => cancelAnimationFrame(rafRef.current), [])\n\n  useEffect(() => {\n    // Intentional: reacts to the reducedMotion media query changing after\n    // mount by synchronously finishing the load animation. startLoading is\n    // also a public event-handler API, so this can't be moved to render.\n    // eslint-disable-next-line react-hooks/set-state-in-effect\n    if (reducedMotion) startLoading()\n  }, [reducedMotion, startLoading])\n\n  return { phase, progress, startLoading, reset }\n}\n",
      "type": "registry:hook",
      "target": "app/google-chrome/hooks/use-simulated-page-load.ts"
    },
    {
      "path": "registry/new-york/blocks/google-chrome/components/google-home-preview.tsx",
      "content": "import { Mic, Plus, ScanSearch, Search, Sparkles } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nimport \"../styles/google-home-preview.css\"\n\nconst iconClass = \"size-[18px] shrink-0 stroke-[1.75]\"\n\ntype GoogleHomePreviewProps = {\n  className?: string\n}\n\nfunction GoogleWordmark() {\n  return (\n    <h1\n      className=\"mb-7 flex items-baseline gap-0 font-[system-ui,-apple-system,'Segoe_UI',Roboto,sans-serif] text-[clamp(2.75rem,8vw,4.25rem)] leading-none font-normal tracking-[-0.04em] select-none\"\n      aria-hidden\n    >\n      <span style={{ color: \"var(--ghp-logo-g)\" }}>G</span>\n      <span style={{ color: \"var(--ghp-logo-o1)\" }}>o</span>\n      <span style={{ color: \"var(--ghp-logo-o2)\" }}>o</span>\n      <span style={{ color: \"var(--ghp-logo-g2)\" }}>g</span>\n      <span style={{ color: \"var(--ghp-logo-l)\" }}>l</span>\n      <span style={{ color: \"var(--ghp-logo-e)\" }}>e</span>\n    </h1>\n  )\n}\n\nexport default function GoogleHomePreview({ className }: GoogleHomePreviewProps) {\n  return (\n    <div\n      className={cn(\n        \"ghp-root flex size-full flex-col items-center justify-start bg-(--ghp-bg) px-4 pt-[16%] pb-10\",\n        className\n      )}\n      aria-hidden\n    >\n      <GoogleWordmark />\n\n      <div\n        className=\"flex w-full max-w-xl items-center gap-2 rounded-full border border-(--ghp-bar-border) bg-(--ghp-bar-bg) py-2.5 pr-2 pl-4 shadow-(--ghp-bar-shadow)\"\n      >\n        <Plus className={cn(iconClass, \"text-(--ghp-icon)\")} aria-hidden />\n        <span className=\"min-w-0 flex-1 truncate text-[0.9375rem] text-(--ghp-placeholder)\">\n          Ask Google\n        </span>\n        <button\n          type=\"button\"\n          tabIndex={-1}\n          className=\"flex size-9 shrink-0 items-center justify-center rounded-full border-none bg-transparent p-0 text-(--ghp-icon)\"\n          aria-hidden\n        >\n          <Mic className={iconClass} />\n        </button>\n        <button\n          type=\"button\"\n          tabIndex={-1}\n          className=\"flex size-9 shrink-0 items-center justify-center rounded-full border-none bg-transparent p-0 text-(--ghp-icon)\"\n          aria-hidden\n        >\n          <ScanSearch className={iconClass} />\n        </button>\n        <button\n          type=\"button\"\n          tabIndex={-1}\n          className=\"flex h-9 shrink-0 items-center gap-1.5 rounded-full border-none bg-(--ghp-ai-bg) px-3.5 text-[0.8125rem] font-medium text-(--ghp-ai-fg)\"\n          aria-hidden\n        >\n          <span className=\"relative flex size-[18px] items-center justify-center\">\n            <Search\n              className=\"size-3.5 stroke-2 text-(--ghp-ai-icon)\"\n              aria-hidden\n            />\n            <Sparkles\n              className=\"absolute -top-0.5 -right-0.5 size-2.5 fill-(--ghp-ai-icon) stroke-none text-(--ghp-ai-icon)\"\n              aria-hidden\n            />\n          </span>\n          AI Mode\n        </button>\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "app/google-chrome/components/google-home-preview.tsx"
    },
    {
      "path": "registry/new-york/blocks/google-chrome/styles/google-home-preview.css",
      "content": "/* Google new-tab preview — light/dark via .dark ancestor */\n\n.ghp-root {\n  --ghp-bg: #ffffff;\n  --ghp-logo-g: #4285f4;\n  --ghp-logo-o1: #ea4335;\n  --ghp-logo-o2: #fbbc05;\n  --ghp-logo-g2: #4285f4;\n  --ghp-logo-l: #34a853;\n  --ghp-logo-e: #ea4335;\n  --ghp-logo-mono: #202124;\n  --ghp-bar-bg: #ffffff;\n  --ghp-bar-border: rgb(218 220 224);\n  --ghp-bar-shadow: 0 1px 6px rgb(32 33 36 / 0.18);\n  --ghp-placeholder: #70757a;\n  --ghp-icon: #5f6368;\n  --ghp-ai-bg: #f1f3f4;\n  --ghp-ai-fg: #202124;\n  --ghp-ai-icon: #1a73e8;\n}\n\n.dark .ghp-root {\n  --ghp-bg: #3c4043;\n  --ghp-logo-g: #ffffff;\n  --ghp-logo-o1: #ffffff;\n  --ghp-logo-o2: #ffffff;\n  --ghp-logo-g2: #ffffff;\n  --ghp-logo-l: #ffffff;\n  --ghp-logo-e: #ffffff;\n  --ghp-logo-mono: #ffffff;\n  --ghp-bar-bg: #ffffff;\n  --ghp-bar-border: transparent;\n  --ghp-bar-shadow: none;\n  --ghp-placeholder: #9aa0a6;\n  --ghp-icon: #5f6368;\n  --ghp-ai-bg: #f1f3f4;\n  --ghp-ai-fg: #202124;\n  --ghp-ai-icon: #1a73e8;\n}\n",
      "type": "registry:file",
      "target": "app/google-chrome/styles/google-home-preview.css"
    },
    {
      "path": "registry/new-york/blocks/google-chrome/components/traffic-lights.tsx",
      "content": "import { Maximize2, Minus, X } from \"lucide-react\"\n\nconst lightIcon = {\n  size: 8,\n  strokeWidth: 3,\n  className: \"gc-light-icon shrink-0 text-black/55\",\n}\n\nexport default function TrafficLights() {\n  return (\n    <div\n      className=\"gc-traffic-lights flex items-center gap-2 self-center pb-[0.15rem]\"\n      aria-hidden\n    >\n      <button\n        type=\"button\"\n        tabIndex={-1}\n        className=\"gc-light flex size-2.75 shrink-0 cursor-default items-center justify-center rounded-full border-none bg-[#ff5f57] p-0 shadow-[inset_0_0_0_0.5px_rgb(0_0_0/0.18),0_0.5px_0_rgb(255_255_255/0.35)] transition-[filter] duration-120 ease-out\"\n        aria-label=\"Close\"\n      >\n        <X {...lightIcon} />\n      </button>\n      <button\n        type=\"button\"\n        tabIndex={-1}\n        className=\"gc-light flex size-2.75 shrink-0 cursor-default items-center justify-center rounded-full border-none bg-[#febc2e] p-0 shadow-[inset_0_0_0_0.5px_rgb(0_0_0/0.18),0_0.5px_0_rgb(255_255_255/0.35)] transition-[filter] duration-120 ease-out\"\n        aria-label=\"Minimize\"\n      >\n        <Minus {...lightIcon} />\n      </button>\n      <button\n        type=\"button\"\n        tabIndex={-1}\n        className=\"gc-light flex size-2.75 shrink-0 cursor-default items-center justify-center rounded-full border-none bg-[#28c840] p-0 shadow-[inset_0_0_0_0.5px_rgb(0_0_0/0.18),0_0.5px_0_rgb(255_255_255/0.35)] transition-[filter] duration-120 ease-out\"\n        aria-label=\"Maximize\"\n      >\n        <Maximize2 {...lightIcon} />\n      </button>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "app/google-chrome/components/traffic-lights.tsx"
    },
    {
      "path": "registry/new-york/blocks/google-chrome/components/profile-avatar.tsx",
      "content": "import { chromeConfig } from \"../lib/config\"\n\nexport default function ProfileAvatar() {\n  return (\n    <div\n      className=\"grid size-8 shrink-0 place-items-center rounded-full\"\n      style={{ background: chromeConfig.profileBg }}\n    >\n      <span className=\"translate-y-[-0.08em] text-sm leading-none font-semibold tracking-normal text-(--gc-profile-letter-fg) lowercase [text-shadow:0_0.5px_0_(--gc-profile-letter-shadow)]\">\n        {chromeConfig.profileInitial}\n      </span>\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "app/google-chrome/components/profile-avatar.tsx"
    },
    {
      "path": "registry/new-york/blocks/google-chrome/components/apps-icon.tsx",
      "content": "const SQUARES = [\n  \"#4285F4\",\n  \"#EA4335\",\n  \"#FBBC05\",\n  \"#34A853\",\n  \"#4285F4\",\n  \"#EA4335\",\n  \"#34A853\",\n  \"#FBBC05\",\n  \"#EA4335\",\n] as const\n\nexport default function AppsIcon() {\n  return (\n    <span className=\"grid shrink-0 grid-cols-3 gap-px\" aria-hidden>\n      {SQUARES.map((fill, i) => (\n        <span\n          key={i}\n          className=\"size-1 rounded-[0.5px]\"\n          style={{ backgroundColor: fill }}\n        />\n      ))}\n    </span>\n  )\n}\n",
      "type": "registry:component",
      "target": "app/google-chrome/components/apps-icon.tsx"
    },
    {
      "path": "registry/new-york/blocks/google-chrome/components/omnibox-typing-text.tsx",
      "content": "import { cn } from \"@/lib/utils\"\n\ntype OmniboxTypingTextProps = {\n  displayText: string\n  showCursor: boolean\n  caretSolid: boolean\n  textClassName?: string\n  caretClassName?: string\n}\n\nexport default function OmniboxTypingText({\n  displayText,\n  showCursor,\n  caretSolid,\n  textClassName = \"min-w-0 truncate text-[inherit] leading-[inherit] text-[var(--gc-omnibox-fg)] select-none\",\n  caretClassName = \"gc-omnibox-caret\",\n}: OmniboxTypingTextProps) {\n  return (\n    <span\n      className=\"inline-flex min-h-[calc(0.8125rem*1.2)] min-w-0 flex-1 items-center overflow-hidden text-3.25 leading-[1.2]\"\n      aria-hidden\n    >\n      <span className={cn(textClassName, !displayText && \"opacity-0\")}>\n        {displayText || \"\\u200b\"}\n      </span>\n      {showCursor ? (\n        <span\n          className={cn(\n            caretClassName,\n            \"ml-px inline-block h-[calc(0.8125rem*1.2)] w-px shrink-0 bg-(--gc-omnibox-caret)\",\n            caretSolid && `${caretClassName}--solid`\n          )}\n          aria-hidden\n        />\n      ) : null}\n    </span>\n  )\n}\n",
      "type": "registry:component",
      "target": "app/google-chrome/components/omnibox-typing-text.tsx"
    },
    {
      "path": "registry/new-york/blocks/google-chrome/hooks/use-omnibox-typing.ts",
      "content": "import { useEffect, useLayoutEffect, useReducer, useRef, useSyncExternalStore } from \"react\"\n\nimport {\n  BETWEEN_MS,\n  DELETE_CHAR_MS_FAST,\n  DELETE_CHAR_MS_SLOW,\n  HOLD_MS,\n  START_DELAY_MS,\n  TYPE_CHAR_MS_FAST,\n  TYPE_CHAR_MS_SLOW,\n} from \"../lib/typing-animation\"\n\nexport type OmniboxTypingOptions = {\n  /** Phrases to cycle through in the omnibox */\n  prompts: readonly string[]\n  typeCharMsFast?: number\n  typeCharMsSlow?: number\n  deleteCharMsFast?: number\n  deleteCharMsSlow?: number\n  holdMs?: number\n  betweenMs?: number\n  startDelayMs?: number\n  /** When false, stops after the first phrase is typed (no delete/retype cycle). */\n  loop?: boolean\n  /** Fired when a phrase finishes typing (entering hold). */\n  onTypedComplete?: () => void\n  /** Fired when the omnibox is cleared (entering between). */\n  onTypedCleared?: () => void\n}\n\ntype Phase = \"idle\" | \"typingIn\" | \"hold\" | \"typingOut\" | \"between\"\n\ntype State = {\n  phase: Phase\n  promptIdx: number\n  charIdx: number\n}\n\ntype Action =\n  | { type: \"ADVANCE_CHAR\" }\n  | { type: \"SET_PHASE\"; phase: Phase; charIdx?: number; promptIdx?: number }\n  | { type: \"REWIND_CHAR\" }\n\nfunction jitter(ms: number, spread = 0.1): number {\n  const delta = ms * spread * (Math.random() * 2 - 1)\n  return Math.max(10, Math.round(ms + delta))\n}\n\nfunction typeDelayMs(charIdx: number, len: number, fast: number, slow: number): number {\n  if (len <= 0) return slow\n  const t = charIdx / len\n  const ease = Math.pow(1 - t, 0.32)\n  return jitter(Math.round(fast + (slow - fast) * ease))\n}\n\nfunction deleteDelayMs(charIdx: number, len: number, fast: number, slow: number): number {\n  if (len <= 0) return slow\n  const t = charIdx / len\n  const ease = Math.pow(t, 0.4)\n  return jitter(Math.round(fast + (slow - fast) * ease))\n}\n\nfunction subscribeReducedMotion(onStoreChange: () => void) {\n  const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\")\n  mq.addEventListener(\"change\", onStoreChange)\n  return () => mq.removeEventListener(\"change\", onStoreChange)\n}\n\nfunction getReducedMotion() {\n  return window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n}\n\nfunction getReducedMotionServer() {\n  return false\n}\n\nexport function useOmniboxTyping({\n  prompts,\n  typeCharMsFast = TYPE_CHAR_MS_FAST,\n  typeCharMsSlow = TYPE_CHAR_MS_SLOW,\n  deleteCharMsFast = DELETE_CHAR_MS_FAST,\n  deleteCharMsSlow = DELETE_CHAR_MS_SLOW,\n  holdMs = HOLD_MS,\n  betweenMs = BETWEEN_MS,\n  startDelayMs = START_DELAY_MS,\n  loop = true,\n  onTypedComplete,\n  onTypedCleared,\n}: OmniboxTypingOptions) {\n  const reducedMotion = useSyncExternalStore(\n    subscribeReducedMotion,\n    getReducedMotion,\n    getReducedMotionServer,\n  )\n\n  const [state, dispatch] = useReducer(reducer, {\n    phase: \"idle\",\n    promptIdx: 0,\n    charIdx: 0,\n  })\n\n  const { phase, promptIdx, charIdx } = state\n  const text = prompts[promptIdx] ?? \"\"\n  const len = text.length\n\n  const displayText = reducedMotion ? (prompts[0] ?? \"\") : text.slice(0, charIdx)\n  const showCursor = !reducedMotion && !(loop === false && phase === \"hold\")\n  const caretSolid = phase === \"typingIn\" || phase === \"typingOut\"\n  const isActive =\n    !reducedMotion &&\n    (phase === \"typingIn\" || phase === \"hold\" || phase === \"typingOut\" || charIdx > 0)\n\n  const prevPhaseRef = useRef<Phase>(phase)\n\n  useEffect(() => {\n    const prev = prevPhaseRef.current\n    prevPhaseRef.current = phase\n\n    if (reducedMotion) return\n    if (phase === \"hold\" && prev !== \"hold\") onTypedComplete?.()\n    if (phase === \"between\" && prev !== \"between\") onTypedCleared?.()\n  }, [phase, reducedMotion, onTypedComplete, onTypedCleared])\n\n  useLayoutEffect(() => {\n    if (reducedMotion || prompts.length === 0) return\n\n    let t: ReturnType<typeof setTimeout>\n\n    switch (phase) {\n      case \"idle\":\n        t = setTimeout(() => dispatch({ type: \"SET_PHASE\", phase: \"typingIn\" }), startDelayMs)\n        break\n\n      case \"typingIn\":\n        if (charIdx < len) {\n          t = setTimeout(\n            () => dispatch({ type: \"ADVANCE_CHAR\" }),\n            typeDelayMs(charIdx, len, typeCharMsFast, typeCharMsSlow),\n          )\n        } else {\n          dispatch({ type: \"SET_PHASE\", phase: \"hold\" })\n        }\n        break\n\n      case \"hold\":\n        if (loop) {\n          t = setTimeout(() => dispatch({ type: \"SET_PHASE\", phase: \"typingOut\" }), holdMs)\n        }\n        break\n\n      case \"typingOut\":\n        if (charIdx > 0) {\n          t = setTimeout(\n            () => dispatch({ type: \"REWIND_CHAR\" }),\n            deleteDelayMs(charIdx, len, deleteCharMsFast, deleteCharMsSlow),\n          )\n        } else {\n          dispatch({ type: \"SET_PHASE\", phase: \"between\", charIdx: 0 })\n        }\n        break\n\n      case \"between\":\n        t = setTimeout(() => {\n          const next = (promptIdx + 1) % prompts.length\n          dispatch({\n            type: \"SET_PHASE\",\n            phase: \"typingIn\",\n            charIdx: 0,\n            promptIdx: next,\n          })\n        }, betweenMs)\n        break\n    }\n\n    return () => clearTimeout(t)\n  }, [\n    reducedMotion,\n    phase,\n    charIdx,\n    len,\n    promptIdx,\n    prompts.length,\n    typeCharMsFast,\n    typeCharMsSlow,\n    deleteCharMsFast,\n    deleteCharMsSlow,\n    holdMs,\n    betweenMs,\n    startDelayMs,\n    loop,\n  ])\n\n  return { displayText, showCursor, caretSolid, isActive }\n}\n\nfunction reducer(state: State, action: Action): State {\n  switch (action.type) {\n    case \"ADVANCE_CHAR\":\n      return { ...state, charIdx: state.charIdx + 1 }\n    case \"REWIND_CHAR\":\n      return { ...state, charIdx: Math.max(0, state.charIdx - 1) }\n    case \"SET_PHASE\":\n      return {\n        phase: action.phase,\n        charIdx: action.charIdx ?? state.charIdx,\n        promptIdx: action.promptIdx ?? state.promptIdx,\n      }\n    default:\n      return state\n  }\n}\n",
      "type": "registry:hook",
      "target": "app/google-chrome/hooks/use-omnibox-typing.ts"
    },
    {
      "path": "registry/new-york/blocks/google-chrome/lib/config.ts",
      "content": "export const chromeConfig = {\n  /** Content area min-height — Windows mockup uses a larger value so total shell height matches */\n  defaultViewportMinHeight: \"32rem\",\n  tabTitle: \"New Tab\",\n  omniboxPlaceholder: \"Search Google or type a URL\",\n  /** Default omnibox typewriter phrases — pass your own to `GoogleChrome` for SaaS demos */\n  omniboxTypingPrompts: [\"facebook.com\"] as const,\n  bookmarksAppsLabel: \"Apps\",\n  profileInitial: \"j\",\n  profileBg: \"#14b8a6\",\n} as const\n",
      "type": "registry:lib",
      "target": "app/google-chrome/lib/config.ts"
    },
    {
      "path": "registry/new-york/blocks/google-chrome/lib/typing-animation.ts",
      "content": "/** Eased type speed (ms per character) */\nexport const TYPE_CHAR_MS_SLOW = 72\nexport const TYPE_CHAR_MS_FAST = 28\n\n/** Eased delete speed (ms per character) */\nexport const DELETE_CHAR_MS_SLOW = 42\nexport const DELETE_CHAR_MS_FAST = 14\n\n/** Pause on full phrase before deleting */\nexport const HOLD_MS = 2200\n\n/** Pause after clearing before typing again */\nexport const BETWEEN_MS = 420\n\n/** Pause before the first keystroke (simulates focus / click) */\nexport const START_DELAY_MS = 680\n",
      "type": "registry:lib",
      "target": "app/google-chrome/lib/typing-animation.ts"
    },
    {
      "path": "registry/new-york/blocks/google-chrome/styles/google-chrome.css",
      "content": "/* google-chrome — theme tokens, traffic-light hover, caret blink, viewport slot children */\n\n.gc-root {\n  --gc-shell-bg: #dee1e6;\n  --gc-border: rgb(0 0 0 / 0.12);\n  --gc-ring: rgb(0 0 0 / 0.06);\n  --gc-shadow: 0 20px 40px -12px rgb(0 0 0 / 0.14);\n  --gc-tabstrip-bg: #dee1e6;\n  --gc-tab-bg: #ffffff;\n  --gc-tab-fg: #3c4043;\n  --gc-tab-close: #5f6368;\n  --gc-toolbar-bg: #ffffff;\n  --gc-toolbar-border: rgb(0 0 0 / 0.08);\n  --gc-nav-fg: #5f6368;\n  --gc-nav-disabled: rgb(95 99 104 / 0.38);\n  --gc-omnibox-bg: #f1f3f4;\n  --gc-omnibox-fg: #202124;\n  --gc-omnibox-caret: #1a73e8;\n  --gc-omnibox-focus-ring: rgb(26 115 232 / 0.18);\n  --gc-menu-fg: #5f6368;\n  --gc-bookmarks-bg: #ffffff;\n  --gc-bookmarks-fg: #3c4043;\n  --gc-viewport-bg: #ffffff;\n  --gc-new-tab-btn: #5f6368;\n  --gc-profile-letter-fg: oklch(0.995 0.01 195);\n  --gc-profile-letter-shadow: oklch(0.25 0.06 195 / 0.22);\n}\n\n.dark .gc-root {\n  --gc-shell-bg: #202124;\n  --gc-border: rgb(255 255 255 / 0.1);\n  --gc-ring: rgb(0 0 0 / 0.35);\n  --gc-shadow: 0 20px 40px -12px rgb(0 0 0 / 0.5);\n  --gc-tabstrip-bg: #202124;\n  --gc-tab-bg: #323639;\n  --gc-tab-fg: #e8eaed;\n  --gc-tab-close: #9aa0a6;\n  --gc-toolbar-bg: #323639;\n  --gc-toolbar-border: rgb(255 255 255 / 0.06);\n  --gc-nav-fg: #9aa0a6;\n  --gc-nav-disabled: rgb(154 160 166 / 0.35);\n  --gc-omnibox-bg: #202124;\n  --gc-omnibox-fg: #e8eaed;\n  --gc-omnibox-caret: #8ab4f8;\n  --gc-omnibox-focus-ring: rgb(138 180 248 / 0.22);\n  --gc-menu-fg: #9aa0a6;\n  --gc-bookmarks-bg: #323639;\n  --gc-bookmarks-fg: #e8eaed;\n  --gc-viewport-bg: #202124;\n  --gc-new-tab-btn: #9aa0a6;\n  --gc-profile-letter-fg: oklch(0.97 0.028 190);\n  --gc-profile-letter-shadow: oklch(0.15 0.04 250 / 0.45);\n}\n\n.gc-traffic-lights:hover .gc-light {\n  filter: brightness(0.92);\n}\n\n.gc-light-icon {\n  opacity: 0;\n  transition: opacity 0.12s ease;\n}\n\n.gc-traffic-lights:hover .gc-light-icon {\n  opacity: 1;\n}\n\n.dark .gc-light {\n  box-shadow:\n    inset 0 0 0 0.5px rgb(0 0 0 / 0.45),\n    0 0.5px 0 rgb(255 255 255 / 0.08);\n}\n\n.dark .gc-traffic-lights:hover .gc-light {\n  filter: brightness(0.88);\n}\n\n.dark .gc-traffic-lights:hover .gc-light-icon {\n  color: rgb(0 0 0 / 0.62);\n}\n\n.gc-omnibox-caret {\n  animation: gc-omnibox-caret-blink 1s step-end infinite;\n}\n\n.gc-omnibox-caret--solid {\n  animation: none;\n  opacity: 1;\n}\n\n@keyframes gc-omnibox-caret-blink {\n  0%,\n  45% {\n    opacity: 1;\n  }\n  55%,\n  100% {\n    opacity: 0;\n  }\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .gc-omnibox-caret {\n    animation: none;\n    opacity: 0;\n  }\n}\n\n.gc-viewport-slot > img,\n.gc-viewport-slot > video,\n.gc-viewport-slot > picture,\n.gc-viewport-slot > iframe,\n.gc-viewport-slot :is(img, video, iframe) {\n  display: block;\n  width: 100%;\n  height: 100%;\n  border: none;\n  object-fit: cover;\n  object-position: center;\n}\n\n.gc-viewport-slot > * {\n  flex: 1;\n  min-width: 0;\n  min-height: 0;\n}\n",
      "type": "registry:file",
      "target": "app/google-chrome/styles/google-chrome.css"
    }
  ],
  "type": "registry:block"
}