{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "laptop-frame",
  "title": "Laptop Frame",
  "description": "MacBook-style laptop that opens, runs a dynamic-island notch notification, then dives the camera into the screen until an image or video fills the frame.",
  "dependencies": [
    "remotion"
  ],
  "registryDependencies": [
    "https://snapcn.dev/r/snap-cn-ui.json"
  ],
  "files": [
    {
      "path": "registry/snap-cn/laptop-frame/index.tsx",
      "content": "\"use client\";\n\nimport type { CSSProperties, ReactNode } from \"react\";\nimport {\n  Easing,\n  getRemotionEnvironment,\n  Img,\n  interpolate,\n  OffthreadVideo,\n  staticFile,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\nimport {\n  mixOklch,\n  type SnapCnTheme,\n  useSnapCnTheme,\n  withAlpha,\n} from \"@/lib/snap-cn-ui\";\n\nexport type LaptopFrameEntrance = \"rise\" | \"open\" | \"none\";\nexport type LaptopFrameFinale = \"none\" | \"zoom-to-screen\";\nexport type NotchPhase = \"idle\" | \"loading\" | \"done\";\n\nexport interface LaptopFrameProps {\n  /** Screen content. Falls back to `screenSrc`, then a built-in hero placeholder. */\n  children?: ReactNode;\n  /**\n   * Image *or* video to fill the screen when `children` is omitted. Videos\n   * (.mp4/.webm/.mov/.m4v) play via `<OffthreadVideo>`, images via `<Img>` —\n   * both cover the screen and fade/un-blur in. A root-relative path\n   * (`/showcase-videos/x.mp4`) is served by Next in the Player and rewritten\n   * through `staticFile()` in a render.\n   */\n  screenSrc?: string;\n  /** How the laptop enters. `open` lifts the lid up from the deck. */\n  entrance?: LaptopFrameEntrance;\n  /**\n   * How the shot ends. `zoom-to-screen` dollies the camera into the screen and\n   * un-tilts it until the content fills the frame — a product \"screen takeover\".\n   * Assumes the frame is the whole composition (the cover scale is derived from\n   * `useVideoConfig`).\n   */\n  finale?: LaptopFrameFinale;\n  /** Lid shell + deck base color. Space-grey by default. */\n  /** Lid colour. Defaults to a dark neutral from the design system. */\n  bezelColor?: string;\n  /** Screen fill behind the content. */\n  /** Screen fill behind `children`. Defaults to the theme's `card`. */\n  screenColor?: string;\n  /** Design-system token overrides — applied to the screen's contents. */\n  theme?: Partial<SnapCnTheme>;\n  mode?: \"light\" | \"dark\";\n  /** Notch status indicator. The macOS system green is the look, not a token. */\n  indicatorColor?: string;\n  /** rotateX of the lid at rest, in degrees (positive tips the top back). */\n  restTilt?: number;\n  /** Lid top-corner radius. The bottom corners stay near-square (the hinge). */\n  radius?: number;\n  /** CSS box-shadow under the deck. Empty string disables it. */\n  shadow?: string;\n  /** Uniform size multiplier for the whole machine. */\n  scale?: number;\n  /** Render the notch notification pill. */\n  showNotch?: boolean;\n  /** Text shown in the notch's connected state. */\n  notchLabel?: string;\n  /** Battery fill (0–100) drawn green in the connected state. */\n  batteryLevel?: number;\n  /** Gentle vertical bob after the entrance settles. */\n  floatLoop?: boolean;\n  /** Peak float displacement in pixels. */\n  floatAmplitude?: number;\n  speed?: number;\n  className?: string;\n}\n\nconst FONT_FAMILY =\n  'Inter, var(--font-geist-sans), -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif';\n\n/** Lid (screen panel) size — a 16:10 MacBook-style display. */\nexport const LID_WIDTH = 820;\nexport const LID_HEIGHT = 500;\n\n/** Dark shell thickness between the lid edge and the screen. */\nexport const BEZEL_WIDTH = 14;\n\n/** Deck (base) is a touch wider than the lid — the trapezoid under the hinge. */\nexport const DECK_WIDTH = 900;\nexport const DECK_HEIGHT = 22;\n\n/** Lid top-corner radius; bottom corners are near-square where the hinge is. */\nexport const LID_RADIUS = 22;\n\n/** rotateX of the lid at rest, and the perspective it is rendered through. */\nexport const REST_TILT = 18;\nexport const PERSPECTIVE = 1600;\n\n/** Frames the entrance takes before the machine is fully settled. */\nexport const ENTRANCE_FRAMES = 30;\n\n/** Camera \"screen takeover\" push: dolly + un-tilt from here to here. */\nexport const PUSH_START = 140;\nexport const PUSH_END = 185;\n\n/**\n * The camera-move easing. A *moderate* decelerate — not quint/expo-out, which\n * cover their travel in the first third and then spend frames moving < 0.5px,\n * i.e. freeze on a frame clock. See the motion-quality skill.\n */\nexport const CAMERA_EASE = Easing.bezier(0.2, 0.6, 0.35, 1);\n\n/**\n * Camera-box height: the lid + deck stack, which is what the outer wrapper\n * shrink-wraps and scales around. Used to find how far the screen centre sits\n * above the box centre so the zoom can recentre it in the frame.\n */\nexport const COLUMN_HEIGHT = LID_HEIGHT + DECK_HEIGHT;\n\n/** Seconds per full float-loop cycle. */\nexport const FLOAT_PERIOD_SECONDS = 5;\n\n/** Notch pill height, and its width in each of the three states. */\nexport const NOTCH_HEIGHT = 30;\nexport const NOTCH_IDLE_WIDTH = 116;\nexport const NOTCH_LOADING_WIDTH = 150;\nexport const NOTCH_DONE_WIDTH = 320;\n\n/** Default frame at which each notch state begins (before `speed`). */\nexport const NOTCH_TIMING = {\n  loadingStart: 72,\n  doneStart: 120,\n} as const;\n\nexport interface EntrancePose {\n  opacity: number;\n  translateY: number;\n  scale: number;\n}\n\n/**\n * Pure entrance schedule for the whole machine. Every entrance settles to the\n * identity pose by `ENTRANCE_FRAMES`, so the float loop takes over with no seam.\n * The lid-open rotation is a separate track — see `lidTilt`.\n */\nexport function entrancePose(\n  frame: number,\n  entrance: LaptopFrameEntrance,\n): EntrancePose {\n  const clamp = {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  } as const;\n  const easeOut = Easing.out(Easing.cubic);\n\n  if (entrance === \"none\") {\n    return { opacity: 1, translateY: 0, scale: 1 };\n  }\n\n  if (entrance === \"open\") {\n    // The travel is small — the drama is in the lid lifting (lidTilt).\n    return {\n      opacity: interpolate(frame, [0, 10], [0, 1], clamp),\n      translateY: interpolate(frame, [0, 24], [24, 0], {\n        ...clamp,\n        easing: easeOut,\n      }),\n      scale: 1,\n    };\n  }\n\n  // rise (default)\n  return {\n    opacity: interpolate(frame, [0, 12], [0, 1], clamp),\n    translateY: interpolate(frame, [0, 26], [70, 0], {\n      ...clamp,\n      easing: easeOut,\n    }),\n    scale: 1,\n  };\n}\n\n/**\n * rotateX of the lid (degrees). `open` swings it from closed-ish to rest, hinged\n * at the deck (the lid's transform-origin is its bottom edge). `zoom-to-screen`\n * then flattens it back to 0 during the push, so the takeover ends on a flat,\n * head-on screen rather than a foreshortened one.\n */\nexport function lidTilt(\n  frame: number,\n  entrance: LaptopFrameEntrance,\n  finale: LaptopFrameFinale = \"none\",\n  restTilt: number = REST_TILT,\n): number {\n  const opened =\n    entrance === \"open\"\n      ? interpolate(frame, [0, ENTRANCE_FRAMES], [82, restTilt], {\n          extrapolateLeft: \"clamp\",\n          extrapolateRight: \"clamp\",\n          easing: Easing.out(Easing.cubic),\n        })\n      : restTilt;\n\n  if (finale !== \"zoom-to-screen\" || frame < PUSH_START) return opened;\n\n  return interpolate(frame, [PUSH_START, PUSH_END], [restTilt, 0], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: CAMERA_EASE,\n  });\n}\n\nexport interface CameraPose {\n  scale: number;\n  translateY: number;\n}\n\n/**\n * The \"screen takeover\" camera. `zoom-to-screen` dollies in (scale) and slides\n * up (translateY) so the screen's inner rect grows to *cover* the composition\n * and stays centred as it does. The scale is derived from the composition size,\n * so the content lands exactly filling the frame — verified by rendering frames,\n * not eyeballed (motion-quality Rule 0).\n */\nexport function cameraPose(\n  frame: number,\n  finale: LaptopFrameFinale,\n  baseScale: number,\n  compWidth: number,\n  compHeight: number,\n): CameraPose {\n  if (finale !== \"zoom-to-screen\") return { scale: 1, translateY: 0 };\n\n  const p = interpolate(frame, [PUSH_START, PUSH_END], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: CAMERA_EASE,\n  });\n\n  // Screen inner, at the resting base scale, must cover the whole frame.\n  const innerW = (LID_WIDTH - 2 * BEZEL_WIDTH) * baseScale;\n  const innerH = (LID_HEIGHT - 2 * BEZEL_WIDTH) * baseScale;\n  const cover = Math.max(compWidth / innerW, compHeight / innerH);\n  const scale = 1 + (cover - 1) * p;\n\n  // The screen centre sits `d` px above the camera-box centre (the deck pulls the\n  // box centre down). Countering `d * cover` as we scale keeps it centred: the\n  // screen centre tracks from (centre − d) to (centre) exactly.\n  const d = (COLUMN_HEIGHT / 2 - LID_HEIGHT / 2) * baseScale;\n  const translateY = d * cover * p;\n\n  return { scale, translateY };\n}\n\n/**\n * Deterministic float-loop offset in pixels. Starts at 0 with an upward drift\n * at `startFrame`, so it blends seamlessly out of the entrance. (Same math as\n * the phone-frame float — kept inline so the copied component has no deps.)\n */\nexport function floatOffset(\n  frame: number,\n  fps: number,\n  amplitude: number,\n  startFrame = 0,\n  periodSeconds: number = FLOAT_PERIOD_SECONDS,\n): number {\n  const local = Math.max(0, frame - startFrame);\n  const value =\n    Math.sin((local / (fps * periodSeconds)) * Math.PI * 2) * amplitude;\n  // Negative = upward drift; the `=== 0` guard avoids returning -0.\n  return value === 0 ? 0 : -value;\n}\n\nexport interface NotchState {\n  phase: NotchPhase;\n  /** Pill width in pixels, morphing between the three states. */\n  width: number;\n  /** Cross-fade weights for the three content layers. */\n  idle: number;\n  loading: number;\n  done: number;\n}\n\n/**\n * Pure notch schedule: which state is showing, the morphing pill width, and the\n * cross-fade weights of the three content layers. The pill holds its loading\n * width, then expands to fit the connected label — the dynamic-island morph.\n */\nexport function notchState(\n  frame: number,\n  timing: { loadingStart: number; doneStart: number } = NOTCH_TIMING,\n): NotchState {\n  const { loadingStart, doneStart } = timing;\n  const clamp = {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  } as const;\n\n  const phase: NotchPhase =\n    frame < loadingStart ? \"idle\" : frame < doneStart ? \"loading\" : \"done\";\n\n  const width = interpolate(\n    frame,\n    [loadingStart - 8, loadingStart, doneStart - 4, doneStart + 12],\n    [\n      NOTCH_IDLE_WIDTH,\n      NOTCH_LOADING_WIDTH,\n      NOTCH_LOADING_WIDTH,\n      NOTCH_DONE_WIDTH,\n    ],\n    { ...clamp, easing: Easing.inOut(Easing.cubic) },\n  );\n\n  const idle = interpolate(\n    frame,\n    [loadingStart - 6, loadingStart],\n    [1, 0],\n    clamp,\n  );\n  const loading = interpolate(\n    frame,\n    [loadingStart - 6, loadingStart, doneStart - 6, doneStart],\n    [0, 1, 1, 0],\n    clamp,\n  );\n  const done = interpolate(\n    frame,\n    [doneStart - 4, doneStart + 6],\n    [0, 1],\n    clamp,\n  );\n\n  return { phase, width, idle, loading, done };\n}\n\n/**\n * Built-in placeholder: a full-bleed product hero. Full-bleed on purpose — the\n * `zoom-to-screen` finale dives into the screen, so the content has to reward\n * filling the frame, not sit as a small card on empty canvas. Elements stagger\n * in during the open, then hold. Pass `imageSrc`/`children` to use your own.\n */\nfunction PlaceholderScreen({ frame, t }: { frame: number; t: SnapCnTheme }) {\n  const rise = (delay: number) => {\n    const r = interpolate(frame, [delay, delay + 14], [0, 1], {\n      extrapolateLeft: \"clamp\",\n      extrapolateRight: \"clamp\",\n      easing: Easing.out(Easing.cubic),\n    });\n    return { opacity: r, transform: `translateY(${(1 - r) * 14}px)` };\n  };\n\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        background: `linear-gradient(165deg, ${t.card} 0%, ${mixOklch(\n          t.card,\n          t.primary,\n          0.06,\n        )} 100%)`,\n        color: t.foreground,\n        display: \"flex\",\n        flexDirection: \"column\",\n      }}\n    >\n      {/* App bar */}\n      <div\n        style={{\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"space-between\",\n          padding: \"22px 34px\",\n          ...rise(6),\n        }}\n      >\n        <div style={{ display: \"flex\", alignItems: \"center\", gap: 9 }}>\n          <div\n            style={{\n              width: 24,\n              height: 24,\n              borderRadius: 7,\n              background: `linear-gradient(135deg, ${t.primary}, ${mixOklch(\n                t.primary,\n                t.foreground,\n                0.18,\n              )})`,\n            }}\n          />\n          <span\n            style={{ fontSize: 17, fontWeight: 700, letterSpacing: \"-0.02em\" }}\n          >\n            Acme\n          </span>\n        </div>\n        <div style={{ display: \"flex\", alignItems: \"center\", gap: 20 }}>\n          {[\"Product\", \"Pricing\", \"Docs\"].map((l) => (\n            <span key={l} style={{ fontSize: 13.5, color: t.mutedForeground }}>\n              {l}\n            </span>\n          ))}\n          <div\n            style={{\n              padding: \"7px 14px\",\n              borderRadius: 8,\n              backgroundColor: t.foreground,\n              color: t.background,\n              fontSize: 13,\n              fontWeight: 600,\n            }}\n          >\n            Sign in\n          </div>\n        </div>\n      </div>\n\n      {/* Hero */}\n      <div\n        style={{\n          flex: 1,\n          display: \"flex\",\n          flexDirection: \"column\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          textAlign: \"center\",\n          padding: \"0 40px\",\n          gap: 18,\n        }}\n      >\n        <div\n          style={{\n            display: \"inline-flex\",\n            alignItems: \"center\",\n            gap: 7,\n            padding: \"5px 12px\",\n            borderRadius: 999,\n            border: `1px solid ${t.border}`,\n            backgroundColor: t.card,\n            fontSize: 12.5,\n            fontWeight: 600,\n            color: t.primary,\n            ...rise(12),\n          }}\n        >\n          <span\n            style={{\n              width: 6,\n              height: 6,\n              borderRadius: 999,\n              backgroundColor: t.primary,\n            }}\n          />\n          Now in public beta\n        </div>\n        <div\n          style={{\n            fontSize: 46,\n            fontWeight: 700,\n            letterSpacing: \"-0.03em\",\n            lineHeight: 1.05,\n            maxWidth: 560,\n            ...rise(16),\n          }}\n        >\n          Ship demos that sell.\n        </div>\n        <div\n          style={{\n            fontSize: 16.5,\n            lineHeight: 1.5,\n            color: t.mutedForeground,\n            maxWidth: 440,\n            ...rise(20),\n          }}\n        >\n          Turn your product into a polished launch video in minutes — no editor,\n          no render farm.\n        </div>\n        <div style={{ display: \"flex\", gap: 10, marginTop: 6, ...rise(24) }}>\n          <div\n            style={{\n              padding: \"11px 20px\",\n              borderRadius: 10,\n              background: `linear-gradient(135deg, ${t.primary}, ${mixOklch(\n                t.primary,\n                t.foreground,\n                0.18,\n              )})`,\n              color: t.primaryForeground,\n              fontSize: 14.5,\n              fontWeight: 600,\n              boxShadow: `0 10px 24px ${withAlpha(t.primary, 0.32)}`,\n            }}\n          >\n            Get started\n          </div>\n          <div\n            style={{\n              padding: \"11px 20px\",\n              borderRadius: 10,\n              border: `1px solid ${t.border}`,\n              backgroundColor: t.card,\n              fontSize: 14.5,\n              fontWeight: 600,\n            }}\n          >\n            Watch demo\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n\nconst isVideo = (src: string) => /\\.(mp4|webm|mov|m4v)(\\?|$)/i.test(src);\n\n/**\n * A root-relative asset (`/showcase-videos/x.mp4`) is served at the origin root\n * by Next in the Player, but a server render serves `public/` through\n * `staticFile()` — so rewrite local paths only while rendering, and pass\n * http(s)/data/blob URLs straight through.\n */\nfunction resolveSrc(src: string): string {\n  const isLocal = src.startsWith(\"/\") && !src.startsWith(\"//\");\n  if (isLocal && getRemotionEnvironment().isRendering) {\n    return staticFile(src.replace(/^\\/+/, \"\"));\n  }\n  return src;\n}\n\n/** Fills the screen with an image or a video, fading and un-blurring it in. */\nfunction ScreenMedia({ src, frame }: { src: string; frame: number }) {\n  const t = interpolate(frame, [4, 22], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n  const style: CSSProperties = {\n    position: \"absolute\",\n    inset: 0,\n    width: \"100%\",\n    height: \"100%\",\n    // Tailwind's preflight sets `img { max-width: 100% }`, which can collapse a\n    // media element we size ourselves to 0px wide. Opt out. (motion-quality skill.)\n    maxWidth: \"none\",\n    objectFit: \"cover\",\n    opacity: t,\n    filter: `blur(${(1 - t) * 8}px)`,\n  };\n  const resolved = resolveSrc(src);\n  return isVideo(src) ? (\n    <OffthreadVideo src={resolved} muted style={style} />\n  ) : (\n    <Img src={resolved} style={style} />\n  );\n}\n\nfunction Dot({\n  size,\n  color,\n  style,\n}: {\n  size: number;\n  color: string;\n  style?: CSSProperties;\n}) {\n  return (\n    <div\n      style={{\n        width: size,\n        height: size,\n        borderRadius: 999,\n        backgroundColor: color,\n        ...style,\n      }}\n    />\n  );\n}\n\n/** The notch pill notification, morphing idle → loading → connected. */\nfunction Notch({\n  frame,\n  label,\n  batteryLevel,\n  t,\n  indicatorColor,\n}: {\n  frame: number;\n  label: string;\n  batteryLevel: number;\n  /** Resolved in dark mode — the notch is a dark pill whatever the app theme. */\n  t: SnapCnTheme;\n  indicatorColor: string;\n}) {\n  const state = notchState(frame);\n  const layer: CSSProperties = {\n    position: \"absolute\",\n    inset: 0,\n    display: \"flex\",\n    alignItems: \"center\",\n    justifyContent: \"center\",\n  };\n\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        top: 9,\n        left: \"50%\",\n        translate: \"-50% 0px\",\n        width: state.width,\n        height: NOTCH_HEIGHT,\n        borderRadius: 999,\n        backgroundColor: t.background,\n        overflow: \"hidden\",\n        zIndex: 10,\n      }}\n    >\n      {/* idle — camera dot + sensor dot */}\n      <div style={{ ...layer, gap: 6, opacity: state.idle }}>\n        <Dot\n          size={9}\n          color={mixOklch(t.background, t.foreground, 0.12)}\n          style={{\n            border: `1px solid ${mixOklch(t.background, t.foreground, 0.2)}`,\n          }}\n        />\n        <Dot size={5} color={mixOklch(t.background, t.foreground, 0.06)} />\n      </div>\n\n      {/* loading — three pulsing dots */}\n      <div style={{ ...layer, gap: 4, opacity: state.loading }}>\n        {[0, 1, 2].map((i) => (\n          <Dot\n            key={i}\n            size={5}\n            color={t.foreground}\n            style={{\n              opacity: 0.4 + 0.6 * (0.5 + 0.5 * Math.sin(frame / 4 - i * 0.9)),\n            }}\n          />\n        ))}\n      </div>\n\n      {/* done — label + battery */}\n      <div style={{ ...layer, gap: 8, opacity: state.done }}>\n        <span\n          style={{\n            fontSize: 12,\n            fontWeight: 500,\n            letterSpacing: \"-0.01em\",\n            color: t.foreground,\n            whiteSpace: \"nowrap\",\n          }}\n        >\n          {label}\n        </span>\n        <div\n          style={{\n            width: 22,\n            height: 11,\n            borderRadius: 3,\n            border: `1.5px solid ${indicatorColor}`,\n            padding: 1.5,\n            display: \"flex\",\n            alignItems: \"center\",\n          }}\n        >\n          <div\n            style={{\n              width: `${Math.max(0, Math.min(100, batteryLevel))}%`,\n              height: \"100%\",\n              borderRadius: 1,\n              backgroundColor: indicatorColor,\n            }}\n          />\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport function LaptopFrame({\n  children,\n  screenSrc,\n  entrance = \"rise\",\n  finale = \"none\",\n  bezelColor,\n  screenColor,\n  theme,\n  mode,\n  indicatorColor = \"#30D158\",\n  restTilt = REST_TILT,\n  radius = LID_RADIUS,\n  shadow,\n  scale = 0.95,\n  showNotch = true,\n  notchLabel = \"AirPods Connected\",\n  batteryLevel = 85,\n  floatLoop = true,\n  floatAmplitude = 4,\n  speed = 1,\n  className,\n}: LaptopFrameProps) {\n  const frame = useCurrentFrame() * speed;\n  // The screen shows an app, so it follows the app theme. The lid, the notch\n  // and the drop shadow are a physical object photographed on a desk: they take\n  // the dark end of the system whatever mode the screen is in.\n  const t = useSnapCnTheme(theme, mode);\n  const shell = useSnapCnTheme(theme, \"dark\");\n  const lid = bezelColor ?? mixOklch(shell.background, shell.foreground, 0.09);\n  const glass = screenColor ?? t.card;\n  const drop = shadow ?? `0 40px 80px ${withAlpha(shell.background, 0.45)}`;\n  const { fps, width, height } = useVideoConfig();\n  const isRendering = getRemotionEnvironment().isRendering;\n\n  const pose = entrancePose(frame, entrance);\n  const tilt = lidTilt(frame, entrance, finale, restTilt);\n  const camera = cameraPose(frame, finale, scale, width, height);\n  // The float loop would fight the camera push, so it stops when the dive starts.\n  const bob =\n    floatLoop && !(finale === \"zoom-to-screen\" && frame >= PUSH_START)\n      ? floatOffset(frame, fps, floatAmplitude, ENTRANCE_FRAMES)\n      : 0;\n  // The notch is at the top of the screen; fade it out as we dive past it.\n  const notchFade =\n    finale === \"zoom-to-screen\"\n      ? interpolate(frame, [PUSH_START, PUSH_START + 10], [1, 0], {\n          extrapolateLeft: \"clamp\",\n          extrapolateRight: \"clamp\",\n        })\n      : 1;\n\n  const screenContent =\n    children ??\n    (screenSrc ? (\n      <ScreenMedia src={screenSrc} frame={frame} />\n    ) : (\n      <PlaceholderScreen frame={frame} t={t} />\n    ));\n\n  return (\n    <div\n      className={className}\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        fontFamily: FONT_FAMILY,\n      }}\n    >\n      {/* Camera: the screen-takeover dolly. `geometricPrecision` keeps any scaled\n          text from boiling; will-change helps the live Player but is wrong in a\n          render (it rasterises type across parallel tabs) — motion-quality skill. */}\n      <div\n        style={{\n          transform: `translateY(${camera.translateY}px) scale(${camera.scale})`,\n          transformOrigin: \"center center\",\n          textRendering: \"geometricPrecision\",\n          ...(isRendering ? {} : { willChange: \"transform\" as const }),\n        }}\n      >\n        {/* Entrance + float rig */}\n        <div\n          style={{\n            opacity: pose.opacity,\n            translate: `0px ${pose.translateY + bob}px`,\n            scale: `${pose.scale * scale}`,\n          }}\n        >\n          {/* Perspective stage: the lid tilts inside it, the deck stays flat */}\n          <div\n            style={{\n              display: \"flex\",\n              flexDirection: \"column\",\n              alignItems: \"center\",\n              perspective: PERSPECTIVE,\n            }}\n          >\n            {/* Lid — hinged at its bottom edge */}\n            <div\n              style={{\n                width: LID_WIDTH,\n                height: LID_HEIGHT,\n                borderRadius: `${radius}px ${radius}px 6px 6px`,\n                backgroundColor: lid,\n                padding: BEZEL_WIDTH,\n                transformOrigin: \"center bottom\",\n                transform: `rotateX(${tilt}deg)`,\n              }}\n            >\n              {/* Screen */}\n              <div\n                style={{\n                  position: \"relative\",\n                  width: \"100%\",\n                  height: \"100%\",\n                  borderRadius: `${Math.max(4, radius - BEZEL_WIDTH)}px ${Math.max(\n                    4,\n                    radius - BEZEL_WIDTH,\n                  )}px 4px 4px`,\n                  backgroundColor: glass,\n                  overflow: \"hidden\",\n                }}\n              >\n                {screenContent}\n                {showNotch && notchFade > 0 && (\n                  <div style={{ opacity: notchFade }}>\n                    <Notch\n                      t={shell}\n                      indicatorColor={indicatorColor}\n                      frame={frame}\n                      label={notchLabel}\n                      batteryLevel={batteryLevel}\n                    />\n                  </div>\n                )}\n              </div>\n            </div>\n\n            {/* Deck — wider than the lid, with the lift-lip on top */}\n            <div\n              style={{\n                position: \"relative\",\n                width: DECK_WIDTH,\n                height: DECK_HEIGHT,\n                borderRadius: \"6px 6px 40px 40px\",\n                background: `linear-gradient(to bottom, ${withAlpha(\n                  shell.foreground,\n                  0.16,\n                )}, ${withAlpha(shell.background, 0)} 34%, ${withAlpha(\n                  shell.background,\n                  0.34,\n                )}), ${lid}`,\n                ...(shadow === \"\" ? {} : { boxShadow: drop }),\n              }}\n            >\n              <div\n                style={{\n                  position: \"absolute\",\n                  top: 0,\n                  left: \"50%\",\n                  translate: \"-50% 0px\",\n                  width: 132,\n                  height: 8,\n                  borderRadius: \"0 0 6px 6px\",\n                  backgroundColor: withAlpha(shell.background, 0.28),\n                }}\n              />\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/snap-cn/laptop-frame.tsx"
    }
  ],
  "type": "registry:component"
}