# Google Chrome (Windows)

> Windows Google Chrome window mock with title-bar tabs, Win32 controls, omnibox with Google G, and Google new-tab preview (light/dark) in the viewport.

A shadcn/ui block from the [@uiception](https://uiception.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/uiception/google-chrome-windows).

- Page, with a live preview: https://shadcn.exchange/r/uiception/google-chrome-windows
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/uiception/google-chrome-windows-light-a0995a6e8b05e2a1.png
- Registry item: https://uiception.com/r/google-chrome-windows.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://uiception.com/r/google-chrome-windows.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`

## Files

### `registry/new-york/blocks/google-chrome-windows/page.tsx`

```tsx
import GoogleChromeWindows from "./components/google-chrome-windows"

export default function Page() {
  return (
    <div className="px-4 py-4 md:px-6 md:py-6">
      <GoogleChromeWindows />
    </div>
  )
}
```

### `registry/new-york/blocks/google-chrome-windows/components/google-chrome-windows.tsx`

```tsx
"use client"

import { useCallback } from "react"

import {
  ArrowLeft,
  ArrowRight,
  MoreVertical,
  Plus,
  Puzzle,
  RotateCw,
  Star,
  X,
} from "lucide-react"

import { cn } from "@/lib/utils"

import BrowserViewport, { type BrowserViewportProps } from "./browser-viewport"
import ChromeViewportDefault from "./chrome-viewport-default"
import GoogleGIcon from "./google-g-icon"
import OmniboxTypingText from "./omnibox-typing-text"
import ProfileAvatar from "./profile-avatar"
import WindowControls from "./window-controls"
import { useOmniboxTyping } from "../hooks/use-omnibox-typing"
import { useSimulatedPageLoad } from "../hooks/use-simulated-page-load"
import { chromeWindowsConfig } from "../lib/config"

import "../styles/google-chrome-windows.css"

const tabIconClass = "size-3.5 shrink-0 stroke-[2]"
const menuIconClass = "size-[18px] shrink-0 stroke-[1.75]"
const toolbarIconSize = 18

const toolbarIcon = {
  size: toolbarIconSize,
  strokeWidth: 1.75,
  className: "shrink-0",
}

function ToolbarSeparator() {
  return (
    <span
      className="mx-1.5 h-4.5 w-px shrink-0 self-center bg-(--gcw-toolbar-sep)"
      aria-hidden
    />
  )
}

type GoogleChromeWindowsProps = {
  className?: string
  omniboxTypingPrompts?: readonly string[]
  /** When false, omnibox stops after typing the first prompt once. */
  omniboxTypingLoop?: boolean
  /** Fired when omnibox typing completes (entering hold). */
  onOmniboxTypedComplete?: () => void
  /** Fired when omnibox text is cleared (entering between). */
  onOmniboxTypedCleared?: () => void
} & Omit<BrowserViewportProps, "className">

export default function GoogleChromeWindows({
  className,
  omniboxTypingPrompts = chromeWindowsConfig.omniboxTypingPrompts,
  omniboxTypingLoop = true,
  onOmniboxTypedComplete,
  onOmniboxTypedCleared,
  minHeight,
  aspectRatio,
  src,
  alt,
  videoSrc,
  poster,
  autoPlay,
  children,
}: GoogleChromeWindowsProps) {
  const hasCustomViewport = children != null
  const { phase, progress, startLoading } = useSimulatedPageLoad()

  const handleTypedComplete = useCallback(() => {
    if (!hasCustomViewport) startLoading()
    onOmniboxTypedComplete?.()
  }, [hasCustomViewport, startLoading, onOmniboxTypedComplete])

  const { displayText, showCursor, caretSolid, isActive } = useOmniboxTyping({
    prompts: omniboxTypingPrompts,
    loop: omniboxTypingLoop,
    onTypedComplete: handleTypedComplete,
    onTypedCleared: onOmniboxTypedCleared,
  })

  return (
    <div
      className={cn(
        "gcw-root mx-auto w-full max-w-6xl min-w-0 overflow-hidden rounded-sm border border-(--gcw-border) bg-(--gcw-chrome-bg) font-['Segoe_UI',system-ui,-apple-system,Roboto,'Helvetica_Neue',Arial,sans-serif] shadow-(--gcw-shadow) outline-none outline-1 -outline-offset-1 outline-(--gcw-ring)",
        className
      )}
      role="img"
      aria-label="Google Chrome on Windows browser window mockup"
    >
      <div className="flex min-h-11 items-stretch justify-between gap-1.5 bg-(--gcw-chrome-bg) pb-0 pl-1.5">
        <div className="flex min-w-0 flex-1 items-end gap-0.5 pt-1">
          <div className="grid w-[clamp(9.5rem,32vw,12rem)] max-w-48 min-w-38 shrink-0 grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-2 rounded-t-lg bg-(--gcw-tab-bg) px-1.5 py-1.5 pl-2.5 text-xs leading-[1.2] text-(--gcw-tab-fg)">
            <GoogleGIcon muted className="size-3.5" />
            <span className="min-w-0 truncate">
              {chromeWindowsConfig.tabTitle}
            </span>
            <button
              type="button"
              className="mr-0.5 flex size-5 items-center justify-center self-center justify-self-end rounded-full border-none bg-transparent p-0 text-(--gcw-tab-close) opacity-85 hover:opacity-100"
              tabIndex={-1}
              aria-hidden
            >
              <X className={tabIconClass} />
            </button>
          </div>
          <button
            type="button"
            className="mb-1.5 flex size-5 shrink-0 items-center justify-center border-none bg-transparent p-0 text-(--gcw-new-tab)"
            tabIndex={-1}
            aria-hidden
          >
            <Plus className={tabIconClass} />
          </button>
        </div>
        <WindowControls />
      </div>

      <div className="box-border flex min-h-10.5 items-center gap-2 border-b border-(--gcw-toolbar-border) bg-(--gcw-toolbar-bg) py-1.5 pr-2.5 pl-3">
        <div className="flex shrink-0 items-center gap-px">
          <button
            type="button"
            className="flex size-8 shrink-0 items-center justify-center rounded-full border-none bg-transparent p-0 text-(--gcw-nav-fg)"
            tabIndex={-1}
            aria-hidden
          >
            <ArrowLeft {...toolbarIcon} />
          </button>
          <button
            type="button"
            className="flex size-8 shrink-0 cursor-default items-center justify-center rounded-full border-none bg-transparent p-0 text-(--gcw-nav-disabled)"
            disabled
            tabIndex={-1}
            aria-hidden
          >
            <ArrowRight {...toolbarIcon} />
          </button>
          <button
            type="button"
            className="flex size-8 shrink-0 items-center justify-center rounded-full border-none bg-transparent p-0 text-(--gcw-nav-fg)"
            tabIndex={-1}
            aria-hidden
          >
            <RotateCw {...toolbarIcon} />
          </button>
        </div>

        <div
          className={cn(
            "flex min-w-0 flex-1 items-center gap-2 rounded-full border border-(--gcw-omnibox-border) bg-(--gcw-omnibox-bg) py-1 pr-2 pl-2.5 transition-[box-shadow,border-color] duration-200 ease-out motion-reduce:transition-none",
            isActive &&
              "border-transparent shadow-[0_0_0_1px_var(--gcw-omnibox-focus-ring)]"
          )}
        >
          <GoogleGIcon className="size-4" />
          <OmniboxTypingText
            displayText={displayText}
            showCursor={showCursor}
            caretSolid={caretSolid}
          />
          <div className="flex shrink-0 items-center gap-0.5" aria-hidden>
            <button
              type="button"
              className="flex size-6.5 items-center justify-center rounded-full border-none bg-transparent p-0 text-(--gcw-omnibox-icon)"
              tabIndex={-1}
            >
              <Star {...toolbarIcon} />
            </button>
          </div>
        </div>

        <div className="flex shrink-0 items-center gap-1">
          <button
            type="button"
            className="flex size-8 shrink-0 items-center justify-center rounded-full border-none bg-transparent p-0 text-(--gcw-toolbar-icon)"
            tabIndex={-1}
            aria-hidden
          >
            <Puzzle {...toolbarIcon} />
          </button>
          <ToolbarSeparator />
          <ProfileAvatar />
          <button
            type="button"
            className="flex size-8 shrink-0 items-center justify-center rounded-full border-none bg-transparent p-0 text-(--gcw-toolbar-icon)"
            tabIndex={-1}
            aria-hidden
          >
            <MoreVertical className={menuIconClass} />
          </button>
        </div>
      </div>

      <BrowserViewport
        minHeight={minHeight}
        aspectRatio={aspectRatio}
        src={src}
        alt={alt}
        videoSrc={videoSrc}
        poster={poster}
        autoPlay={autoPlay}
      >
        {children ?? (
          <ChromeViewportDefault phase={phase} progress={progress} />
        )}
      </BrowserViewport>
    </div>
  )
}
```

### `registry/new-york/blocks/google-chrome-windows/components/browser-viewport.tsx`

```tsx
import type { CSSProperties, ReactNode } from "react"

import { cn } from "@/lib/utils"

import { chromeWindowsConfig } from "../lib/config"
import GoogleHomePreview from "./google-home-preview"

export type BrowserViewportProps = {
  className?: string
  minHeight?: string | number
  aspectRatio?: string
  src?: string
  alt?: string
  videoSrc?: string
  poster?: string
  autoPlay?: boolean
  children?: ReactNode
}

export default function BrowserViewport({
  className,
  minHeight = chromeWindowsConfig.defaultViewportMinHeight,
  aspectRatio,
  src,
  alt = "",
  videoSrc,
  poster,
  autoPlay = true,
  children,
}: BrowserViewportProps) {
  const style = {
    ...(aspectRatio ? { aspectRatio } : {}),
    minHeight: typeof minHeight === "number" ? `${minHeight}px` : minHeight,
  } satisfies CSSProperties

  return (
    <div
      className={cn(
        "relative w-full overflow-hidden bg-(--gcw-viewport-bg)",
        className
      )}
      style={style}
    >
      {children ? (
        <div className="gcw-viewport-slot absolute inset-0 flex size-full overflow-hidden">
          {children}
        </div>
      ) : videoSrc ? (
        <video
          className="absolute inset-0 block size-full border-none object-cover object-center"
          src={videoSrc}
          poster={poster}
          muted
          loop
          playsInline
          autoPlay={autoPlay}
        />
      ) : src ? (
        // eslint-disable-next-line @next/next/no-img-element -- registry block; consumers may swap for next/image
        <img
          className="absolute inset-0 block size-full border-none object-cover object-center"
          src={src}
          alt={alt}
        />
      ) : (
        <GoogleHomePreview className="absolute inset-0" />
      )}
    </div>
  )
}
```

### `registry/new-york/blocks/google-chrome-windows/components/chrome-viewport-default.tsx`

```tsx
import GoogleHomePreview from "./google-home-preview"
import PageLoadingBar from "./page-loading-bar"
import type { PageLoadPhase } from "../hooks/use-simulated-page-load"

type ChromeViewportDefaultProps = {
  progress: number
  phase: PageLoadPhase
}

export default function ChromeViewportDefault({
  progress,
  phase,
}: ChromeViewportDefaultProps) {
  return (
    <div className="relative size-full">
      <PageLoadingBar progress={progress} visible={phase === "loading"} />
      <GoogleHomePreview className="absolute inset-0" />
    </div>
  )
}
```

### `registry/new-york/blocks/google-chrome-windows/components/page-loading-bar.tsx`

```tsx
type PageLoadingBarProps = {
  progress: number
  visible: boolean
}

export default function PageLoadingBar({ progress, visible }: PageLoadingBarProps) {
  if (!visible) return null

  return (
    <div
      className="pointer-events-none absolute inset-x-0 top-0 z-10 h-px overflow-hidden"
      aria-hidden
    >
      <div
        className="h-full bg-[#1a73e8] dark:bg-[#8ab4f8]"
        style={{ width: `${Math.min(100, progress)}%` }}
      />
    </div>
  )
}
```

### `registry/new-york/blocks/google-chrome-windows/hooks/use-simulated-page-load.ts`

```ts
"use client"

import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react"

export type PageLoadPhase = "idle" | "loading" | "loaded"

function subscribeReducedMotion(onStoreChange: () => void) {
  const mq = window.matchMedia("(prefers-reduced-motion: reduce)")
  mq.addEventListener("change", onStoreChange)
  return () => mq.removeEventListener("change", onStoreChange)
}

function getReducedMotion() {
  return window.matchMedia("(prefers-reduced-motion: reduce)").matches
}

function getReducedMotionServer() {
  return false
}

/** Chrome-like load curve: fast start, slow crawl, quick finish. */
function chromeLoadProgress(elapsedMs: number): number {
  if (elapsedMs < 350) {
    return (elapsedMs / 350) * 68
  }
  if (elapsedMs < 1400) {
    const t = (elapsedMs - 350) / 1050
    return 68 + t * 22
  }
  if (elapsedMs < 1650) {
    const t = (elapsedMs - 1400) / 250
    return 90 + t * 10
  }
  return 100
}

export function useSimulatedPageLoad() {
  const reducedMotion = useSyncExternalStore(
    subscribeReducedMotion,
    getReducedMotion,
    getReducedMotionServer,
  )
  const [phase, setPhase] = useState<PageLoadPhase>("idle")
  const [progress, setProgress] = useState(0)
  const rafRef = useRef<number>(0)
  const startRef = useRef<number>(0)

  const reset = useCallback(() => {
    cancelAnimationFrame(rafRef.current)
    setPhase("idle")
    setProgress(0)
  }, [])

  const startLoading = useCallback(() => {
    cancelAnimationFrame(rafRef.current)
    if (reducedMotion) {
      setPhase("loaded")
      setProgress(100)
      return
    }
    setPhase("loading")
    setProgress(0)
    startRef.current = performance.now()

    const tick = (now: number) => {
      const elapsed = now - startRef.current
      const next = chromeLoadProgress(elapsed)
      setProgress(next)
      if (next >= 100) {
        setPhase("loaded")
        return
      }
      rafRef.current = requestAnimationFrame(tick)
    }
    rafRef.current = requestAnimationFrame(tick)
  }, [reducedMotion])

  useEffect(() => () => cancelAnimationFrame(rafRef.current), [])

  useEffect(() => {
    // Intentional: reacts to the reducedMotion media query changing after
    // mount by synchronously finishing the load animation. startLoading is
    // also a public event-handler API, so this can't be moved to render.
    // eslint-disable-next-line react-hooks/set-state-in-effect
    if (reducedMotion) startLoading()
  }, [reducedMotion, startLoading])

  return { phase, progress, startLoading, reset }
}
```

### `registry/new-york/blocks/google-chrome-windows/components/google-home-preview.tsx`

```tsx
import { Mic, Plus, ScanSearch, Search, Sparkles } from "lucide-react"

import { cn } from "@/lib/utils"

import "../styles/google-home-preview.css"

const iconClass = "size-[18px] shrink-0 stroke-[1.75]"

type GoogleHomePreviewProps = {
  className?: string
}

function GoogleWordmark() {
  return (
    <h1
      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"
      aria-hidden
    >
      <span style={{ color: "var(--ghp-logo-g)" }}>G</span>
      <span style={{ color: "var(--ghp-logo-o1)" }}>o</span>
      <span style={{ color: "var(--ghp-logo-o2)" }}>o</span>
      <span style={{ color: "var(--ghp-logo-g2)" }}>g</span>
      <span style={{ color: "var(--ghp-logo-l)" }}>l</span>
      <span style={{ color: "var(--ghp-logo-e)" }}>e</span>
    </h1>
  )
}

export default function GoogleHomePreview({ className }: GoogleHomePreviewProps) {
  return (
    <div
      className={cn(
        "ghp-root flex size-full flex-col items-center justify-start bg-(--ghp-bg) px-4 pt-[16%] pb-10",
        className
      )}
      aria-hidden
    >
      <GoogleWordmark />

      <div
        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)"
      >
        <Plus className={cn(iconClass, "text-(--ghp-icon)")} aria-hidden />
        <span className="min-w-0 flex-1 truncate text-[0.9375rem] text-(--ghp-placeholder)">
          Ask Google
        </span>
        <button
          type="button"
          tabIndex={-1}
          className="flex size-9 shrink-0 items-center justify-center rounded-full border-none bg-transparent p-0 text-(--ghp-icon)"
          aria-hidden
        >
          <Mic className={iconClass} />
        </button>
        <button
          type="button"
          tabIndex={-1}
          className="flex size-9 shrink-0 items-center justify-center rounded-full border-none bg-transparent p-0 text-(--ghp-icon)"
          aria-hidden
        >
          <ScanSearch className={iconClass} />
        </button>
        <button
          type="button"
          tabIndex={-1}
          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)"
          aria-hidden
        >
          <span className="relative flex size-[18px] items-center justify-center">
            <Search
              className="size-3.5 stroke-2 text-(--ghp-ai-icon)"
              aria-hidden
            />
            <Sparkles
              className="absolute -top-0.5 -right-0.5 size-2.5 fill-(--ghp-ai-icon) stroke-none text-(--ghp-ai-icon)"
              aria-hidden
            />
          </span>
          AI Mode
        </button>
      </div>
    </div>
  )
}
```

### `registry/new-york/blocks/google-chrome-windows/styles/google-home-preview.css`

```css
/* Google new-tab preview — light/dark via .dark ancestor */

.ghp-root {
  --ghp-bg: #ffffff;
  --ghp-logo-g: #4285f4;
  --ghp-logo-o1: #ea4335;
  --ghp-logo-o2: #fbbc05;
  --ghp-logo-g2: #4285f4;
  --ghp-logo-l: #34a853;
  --ghp-logo-e: #ea4335;
  --ghp-logo-mono: #202124;
  --ghp-bar-bg: #ffffff;
  --ghp-bar-border: rgb(218 220 224);
  --ghp-bar-shadow: 0 1px 6px rgb(32 33 36 / 0.18);
  --ghp-placeholder: #70757a;
  --ghp-icon: #5f6368;
  --ghp-ai-bg: #f1f3f4;
  --ghp-ai-fg: #202124;
  --ghp-ai-icon: #1a73e8;
}

.dark .ghp-root {
  --ghp-bg: #3c4043;
  --ghp-logo-g: #ffffff;
  --ghp-logo-o1: #ffffff;
  --ghp-logo-o2: #ffffff;
  --ghp-logo-g2: #ffffff;
  --ghp-logo-l: #ffffff;
  --ghp-logo-e: #ffffff;
  --ghp-logo-mono: #ffffff;
  --ghp-bar-bg: #ffffff;
  --ghp-bar-border: transparent;
  --ghp-bar-shadow: none;
  --ghp-placeholder: #9aa0a6;
  --ghp-icon: #5f6368;
  --ghp-ai-bg: #f1f3f4;
  --ghp-ai-fg: #202124;
  --ghp-ai-icon: #1a73e8;
}
```

### `registry/new-york/blocks/google-chrome-windows/components/window-controls.tsx`

```tsx
import { Copy, Minus, X } from "lucide-react"

const iconClass = "size-3.5 shrink-0 stroke-[1.5]"

export default function WindowControls() {
  return (
    <div
      className="ml-auto flex h-11 shrink-0 items-stretch self-stretch"
      aria-hidden
    >
      <button
        type="button"
        tabIndex={-1}
        className="flex h-full w-11 items-center justify-center rounded-none border-none bg-transparent p-0 text-(--gcw-win-fg) transition-[background-color,color] duration-120 ease-out hover:bg-(--gcw-win-hover-muted)"
      >
        <Minus className={iconClass} />
      </button>
      <button
        type="button"
        tabIndex={-1}
        className="flex h-full w-11 items-center justify-center rounded-none border-none bg-transparent p-0 text-(--gcw-win-fg) transition-[background-color,color] duration-120 ease-out hover:bg-(--gcw-win-hover-muted)"
      >
        <Copy className={iconClass} />
      </button>
      <button
        type="button"
        tabIndex={-1}
        className="flex h-full w-11 items-center justify-center rounded-none border-none bg-transparent p-0 text-(--gcw-win-fg) transition-[background-color,color] duration-120 ease-out hover:bg-(--gcw-win-hover-close) hover:text-(--gcw-win-hover-close-fg)"
      >
        <X className={iconClass} />
      </button>
    </div>
  )
}
```

### `registry/new-york/blocks/google-chrome-windows/components/profile-avatar.tsx`

```tsx
import { chromeWindowsConfig } from "../lib/config"

export default function ProfileAvatar() {
  return (
    <span
      className="grid size-8 shrink-0 place-items-center rounded-full"
      style={{ backgroundColor: chromeWindowsConfig.profileBg }}
      aria-hidden
    >
      <span className="translate-y-[-0.08em] text-sm leading-none font-semibold text-(--gcw-profile-letter-fg) lowercase [text-shadow:0_0.5px_0_var(--gcw-profile-letter-shadow)]">
        {chromeWindowsConfig.profileInitial}
      </span>
    </span>
  )
}
```

### `registry/new-york/blocks/google-chrome-windows/components/google-g-icon.tsx`

```tsx
import { cn } from "@/lib/utils"

type GoogleGIconProps = {
  muted?: boolean
  className?: string
}

export default function GoogleGIcon({
  muted = false,
  className,
}: GoogleGIconProps) {
  return (
    <svg
      className={cn(
        "size-4.5 shrink-0",
        muted && "opacity-72 grayscale",
        className
      )}
      viewBox="0 0 24 24"
      aria-hidden
      focusable="false"
    >
      <path
        fill="#4285F4"
        d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z"
      />
      <path
        fill="#34A853"
        d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
      />
      <path
        fill="#FBBC05"
        d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
      />
      <path
        fill="#EA4335"
        d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
      />
    </svg>
  )
}
```

### `registry/new-york/blocks/google-chrome-windows/components/omnibox-typing-text.tsx`

```tsx
import { cn } from "@/lib/utils"

type OmniboxTypingTextProps = {
  displayText: string
  showCursor: boolean
  caretSolid: boolean
  textClassName?: string
  caretClassName?: string
}

export default function OmniboxTypingText({
  displayText,
  showCursor,
  caretSolid,
  textClassName = "min-w-0 truncate text-3.25 leading-[1.2] text-[var(--gcw-omnibox-typed)] select-none",
  caretClassName = "gcw-omnibox-caret",
}: OmniboxTypingTextProps) {
  return (
    <span
      className="inline-flex min-w-0 flex-1 items-center overflow-hidden"
      aria-hidden
    >
      {displayText ? (
        <span className={textClassName}>{displayText}</span>
      ) : null}
      {showCursor ? (
        <span
          className={cn(
            caretClassName,
            "ml-px inline-block h-[1.05em] w-px shrink-0 translate-y-px bg-(--gcw-omnibox-caret) align-text-bottom",
            caretSolid && `${caretClassName}--solid`
          )}
          aria-hidden
        />
      ) : null}
    </span>
  )
}
```

### `registry/new-york/blocks/google-chrome-windows/hooks/use-omnibox-typing.ts`

```ts
import { useEffect, useLayoutEffect, useReducer, useRef, useSyncExternalStore } from "react"

import {
  BETWEEN_MS,
  DELETE_CHAR_MS_FAST,
  DELETE_CHAR_MS_SLOW,
  HOLD_MS,
  START_DELAY_MS,
  TYPE_CHAR_MS_FAST,
  TYPE_CHAR_MS_SLOW,
} from "../lib/typing-animation"

export type OmniboxTypingOptions = {
  /** Phrases to cycle through in the omnibox */
  prompts: readonly string[]
  typeCharMsFast?: number
  typeCharMsSlow?: number
  deleteCharMsFast?: number
  deleteCharMsSlow?: number
  holdMs?: number
  betweenMs?: number
  startDelayMs?: number
  /** When false, stops after the first phrase is typed (no delete/retype cycle). */
  loop?: boolean
  /** Fired when a phrase finishes typing (entering hold). */
  onTypedComplete?: () => void
  /** Fired when the omnibox is cleared (entering between). */
  onTypedCleared?: () => void
}

type Phase = "idle" | "typingIn" | "hold" | "typingOut" | "between"

type State = {
  phase: Phase
  promptIdx: number
  charIdx: number
}

type Action =
  | { type: "ADVANCE_CHAR" }
  | { type: "SET_PHASE"; phase: Phase; charIdx?: number; promptIdx?: number }
  | { type: "REWIND_CHAR" }

function jitter(ms: number, spread = 0.1): number {
  const delta = ms * spread * (Math.random() * 2 - 1)
  return Math.max(10, Math.round(ms + delta))
}

function typeDelayMs(charIdx: number, len: number, fast: number, slow: number): number {
  if (len <= 0) return slow
  const t = charIdx / len
  const ease = Math.pow(1 - t, 0.32)
  return jitter(Math.round(fast + (slow - fast) * ease))
}

function deleteDelayMs(charIdx: number, len: number, fast: number, slow: number): number {
  if (len <= 0) return slow
  const t = charIdx / len
  const ease = Math.pow(t, 0.4)
  return jitter(Math.round(fast + (slow - fast) * ease))
}

function subscribeReducedMotion(onStoreChange: () => void) {
  const mq = window.matchMedia("(prefers-reduced-motion: reduce)")
  mq.addEventListener("change", onStoreChange)
  return () => mq.removeEventListener("change", onStoreChange)
}

function getReducedMotion() {
  return window.matchMedia("(prefers-reduced-motion: reduce)").matches
}

function getReducedMotionServer() {
  return false
}

export function useOmniboxTyping({
  prompts,
  typeCharMsFast = TYPE_CHAR_MS_FAST,
  typeCharMsSlow = TYPE_CHAR_MS_SLOW,
  deleteCharMsFast = DELETE_CHAR_MS_FAST,
  deleteCharMsSlow = DELETE_CHAR_MS_SLOW,
  holdMs = HOLD_MS,
  betweenMs = BETWEEN_MS,
  startDelayMs = START_DELAY_MS,
  loop = true,
  onTypedComplete,
  onTypedCleared,
}: OmniboxTypingOptions) {
  const reducedMotion = useSyncExternalStore(
    subscribeReducedMotion,
    getReducedMotion,
    getReducedMotionServer,
  )

  const [state, dispatch] = useReducer(reducer, {
    phase: "idle",
    promptIdx: 0,
    charIdx: 0,
  })

  const { phase, promptIdx, charIdx } = state
  const text = prompts[promptIdx] ?? ""
  const len = text.length

  const displayText = reducedMotion ? (prompts[0] ?? "") : text.slice(0, charIdx)
  const showCursor = !reducedMotion && !(loop === false && phase === "hold")
  const caretSolid = phase === "typingIn" || phase === "typingOut"
  const isActive =
    !reducedMotion &&
    (phase === "typingIn" || phase === "hold" || phase === "typingOut" || charIdx > 0)

  const prevPhaseRef = useRef<Phase>(phase)

  useEffect(() => {
    const prev = prevPhaseRef.current
    prevPhaseRef.current = phase

    if (reducedMotion) return
    if (phase === "hold" && prev !== "hold") onTypedComplete?.()
    if (phase === "between" && prev !== "between") onTypedCleared?.()
  }, [phase, reducedMotion, onTypedComplete, onTypedCleared])

  useLayoutEffect(() => {
    if (reducedMotion || prompts.length === 0) return

    let t: ReturnType<typeof setTimeout>

    switch (phase) {
      case "idle":
        t = setTimeout(() => dispatch({ type: "SET_PHASE", phase: "typingIn" }), startDelayMs)
        break

      case "typingIn":
        if (charIdx < len) {
          t = setTimeout(
            () => dispatch({ type: "ADVANCE_CHAR" }),
            typeDelayMs(charIdx, len, typeCharMsFast, typeCharMsSlow),
          )
        } else {
          dispatch({ type: "SET_PHASE", phase: "hold" })
        }
        break

      case "hold":
        if (loop) {
          t = setTimeout(() => dispatch({ type: "SET_PHASE", phase: "typingOut" }), holdMs)
        }
        break

      case "typingOut":
        if (charIdx > 0) {
          t = setTimeout(
            () => dispatch({ type: "REWIND_CHAR" }),
            deleteDelayMs(charIdx, len, deleteCharMsFast, deleteCharMsSlow),
          )
        } else {
          dispatch({ type: "SET_PHASE", phase: "between", charIdx: 0 })
        }
        break

      case "between":
        t = setTimeout(() => {
          const next = (promptIdx + 1) % prompts.length
          dispatch({
            type: "SET_PHASE",
            phase: "typingIn",
            charIdx: 0,
            promptIdx: next,
          })
        }, betweenMs)
        break
    }

    return () => clearTimeout(t)
  }, [
    reducedMotion,
    phase,
    charIdx,
    len,
    promptIdx,
    prompts.length,
    typeCharMsFast,
    typeCharMsSlow,
    deleteCharMsFast,
    deleteCharMsSlow,
    holdMs,
    betweenMs,
    startDelayMs,
    loop,
  ])

  return { displayText, showCursor, caretSolid, isActive }
}

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case "ADVANCE_CHAR":
      return { ...state, charIdx: state.charIdx + 1 }
    case "REWIND_CHAR":
      return { ...state, charIdx: Math.max(0, state.charIdx - 1) }
    case "SET_PHASE":
      return {
        phase: action.phase,
        charIdx: action.charIdx ?? state.charIdx,
        promptIdx: action.promptIdx ?? state.promptIdx,
      }
    default:
      return state
  }
}
```

### `registry/new-york/blocks/google-chrome-windows/lib/config.ts`

```ts
export const chromeWindowsConfig = {
  /**
   * Taller than macOS viewport min so total shell height matches `google-chrome`
   * (macOS adds a bookmarks bar; Windows titlebar/toolbar are aligned to mac min-heights).
   */
  defaultViewportMinHeight: "33.75rem",
  tabTitle: "New Tab",
  omniboxPlaceholder: "Ask Google or type a URL",
  /** Default omnibox typewriter phrases — pass your own to `GoogleChromeWindows` for SaaS demos */
  omniboxTypingPrompts: ["youtube.com"] as const,
  profileInitial: "j",
  profileBg: "#14b8a6",
} as const
```

### `registry/new-york/blocks/google-chrome-windows/lib/typing-animation.ts`

```ts
/** Eased type speed (ms per character) */
export const TYPE_CHAR_MS_SLOW = 72
export const TYPE_CHAR_MS_FAST = 28

/** Eased delete speed (ms per character) */
export const DELETE_CHAR_MS_SLOW = 42
export const DELETE_CHAR_MS_FAST = 14

/** Pause on full phrase before deleting */
export const HOLD_MS = 2200

/** Pause after clearing before typing again */
export const BETWEEN_MS = 420

/** Pause before the first keystroke (simulates focus / click) */
export const START_DELAY_MS = 680
```

### `registry/new-york/blocks/google-chrome-windows/styles/google-chrome-windows.css`

```css
/* google-chrome-windows — theme tokens, caret blink, viewport slot children */

.gcw-root {
  --gcw-chrome-bg: #dee1e6;
  --gcw-border: rgb(0 0 0 / 0.14);
  --gcw-ring: rgb(0 0 0 / 0.06);
  --gcw-shadow: 0 20px 40px -12px rgb(0 0 0 / 0.14);
  --gcw-tab-bg: #ffffff;
  --gcw-tab-fg: #202124;
  --gcw-tab-close: #5f6368;
  --gcw-new-tab: #5f6368;
  --gcw-win-fg: #202124;
  --gcw-win-hover-muted: rgb(0 0 0 / 0.06);
  --gcw-win-hover-close: #e81123;
  --gcw-win-hover-close-fg: #ffffff;
  --gcw-toolbar-bg: #ffffff;
  --gcw-toolbar-border: rgb(0 0 0 / 0.08);
  --gcw-nav-fg: #5f6368;
  --gcw-nav-disabled: rgb(95 99 104 / 0.38);
  --gcw-omnibox-bg: #f1f3f4;
  --gcw-omnibox-border: transparent;
  --gcw-omnibox-typed: #202124;
  --gcw-omnibox-caret: #1a73e8;
  --gcw-omnibox-focus-ring: rgb(26 115 232 / 0.18);
  --gcw-omnibox-icon: #5f6368;
  --gcw-toolbar-icon: #5f6368;
  --gcw-toolbar-sep: rgb(0 0 0 / 0.12);
  --gcw-viewport-bg: #ffffff;
  --gcw-profile-letter-fg: oklch(0.995 0.01 195);
  --gcw-profile-letter-shadow: oklch(0.25 0.06 195 / 0.22);
}

.dark .gcw-root {
  --gcw-chrome-bg: #202124;
  --gcw-border: rgb(255 255 255 / 0.1);
  --gcw-ring: rgb(0 0 0 / 0.35);
  --gcw-shadow: 0 20px 40px -12px rgb(0 0 0 / 0.5);
  --gcw-tab-bg: #35363a;
  --gcw-tab-fg: #e8eaed;
  --gcw-tab-close: #9aa0a6;
  --gcw-new-tab: #9aa0a6;
  --gcw-win-fg: #e8eaed;
  --gcw-win-hover-muted: rgb(255 255 255 / 0.1);
  --gcw-win-hover-close: #c42b1c;
  --gcw-win-hover-close-fg: #ffffff;
  --gcw-toolbar-bg: #202124;
  --gcw-toolbar-border: rgb(255 255 255 / 0.06);
  --gcw-nav-fg: #9aa0a6;
  --gcw-nav-disabled: rgb(154 160 166 / 0.35);
  --gcw-omnibox-bg: #35363a;
  --gcw-omnibox-border: rgb(255 255 255 / 0.1);
  --gcw-omnibox-typed: #e8eaed;
  --gcw-omnibox-caret: #8ab4f8;
  --gcw-omnibox-focus-ring: rgb(138 180 248 / 0.22);
  --gcw-omnibox-icon: #9aa0a6;
  --gcw-toolbar-icon: #9aa0a6;
  --gcw-toolbar-sep: rgb(255 255 255 / 0.12);
  --gcw-viewport-bg: #202124;
  --gcw-profile-letter-fg: oklch(0.97 0.028 190);
  --gcw-profile-letter-shadow: oklch(0.15 0.04 250 / 0.45);
}

.gcw-omnibox-caret {
  animation: gcw-omnibox-caret-blink 1s step-end infinite;
}

.gcw-omnibox-caret--solid {
  animation: none;
  opacity: 1;
}

@keyframes gcw-omnibox-caret-blink {
  0%,
  45% {
    opacity: 1;
  }
  55%,
  100% {
    opacity: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .gcw-omnibox-caret {
    animation: none;
    opacity: 0;
  }
}

.gcw-viewport-slot > img,
.gcw-viewport-slot > video,
.gcw-viewport-slot > picture,
.gcw-viewport-slot > iframe,
.gcw-viewport-slot :is(img, video, iframe) {
  display: block;
  width: 100%;
  height: 100%;
  border: none;
  object-fit: cover;
  object-position: center;
}

.gcw-viewport-slot > * {
  flex: 1;
  min-width: 0;
  min-height: 0;
}
```
