{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tablet-frame",
  "title": "Tablet Frame",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A modern tablet drawn in CSS: brushed aluminum frame, thin glass bezel, front camera, status bar and home indicator. Landscape or portrait with the same proportions; children render as the screen at tablet resolution, scaled to fit.",
  "dependencies": [
    "motion@^12"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/media.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/tablet-frame.tsx",
      "content": "// Ballmac UI: Tablet Frame. https://ui.ballmac.com/components/tablet-frame\n\"use client\"\n\nimport * as React from \"react\"\nimport { useReducedMotion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Media } from \"@/components/ballmac/media\"\n\ntype TabletFrameProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  /** Screen content, laid out below the status bar. Ignored when `src` or `videoSrc` is set. */\n  children?: React.ReactNode\n  /** Which way the tablet is held. The frame keeps the real aspect ratio either way. */\n  orientation?: \"landscape\" | \"portrait\"\n  /** Finish of the aluminum frame. \"auto\" is silver in light mode and space black in dark mode. */\n  variant?: \"auto\" | \"silver\" | \"black\"\n  /** Image shown on the screen, cropped to fill it. */\n  src?: string\n  /** Alternative text for `src`. */\n  alt?: string\n  /** Video shown on the screen: muted, looping, and paused under reduced motion. */\n  videoSrc?: string\n  /** Poster image for `videoSrc`. */\n  poster?: string\n  /**\n   * Width in CSS pixels that the screen content is laid out at (1194 landscape or 834 portrait match an 11\" tablet);\n   * it is then scaled to fit. Omit to lay content out at the frame's actual size.\n   */\n  screenWidth?: number\n  /** Show the status bar (time, date, Wi-Fi, battery). */\n  statusBar?: boolean\n  /** Time shown in the status bar. */\n  time?: string\n  /** Date shown next to the time. */\n  date?: string\n  /** Show the home indicator bar at the bottom of the screen. */\n  homeIndicator?: boolean\n  /** Class names for the screen element (inside the bezel). Use \"dark\" to give the screen its own theme. */\n  screenClassName?: string\n}\n\nconst SILVER =\n  \"[--tf-hi:color-mix(in_oklch,white_96%,black)] [--tf-mid:color-mix(in_oklch,white_82%,black)] [--tf-lo:color-mix(in_oklch,white_62%,black)]\"\nconst BLACK =\n  \"[--tf-hi:color-mix(in_oklch,white_36%,black)] [--tf-mid:color-mix(in_oklch,white_18%,black)] [--tf-lo:color-mix(in_oklch,white_9%,black)]\"\nconst BLACK_DARK =\n  \"dark:[--tf-hi:color-mix(in_oklch,white_36%,black)] dark:[--tf-mid:color-mix(in_oklch,white_18%,black)] dark:[--tf-lo:color-mix(in_oklch,white_9%,black)]\"\n\n/** Lays screen content out at `screenWidth` CSS pixels and scales it to fit, without re-rendering. */\nfunction useScreenScale(screenWidth: number | undefined) {\n  const ref = React.useRef<HTMLDivElement>(null)\n  React.useEffect(() => {\n    const screen = ref.current\n    if (!screen || !screenWidth) return\n    const measure = () => screen.style.setProperty(\"--screen-scale\", String(screen.clientWidth / screenWidth))\n    measure()\n    const observer = new ResizeObserver(measure)\n    observer.observe(screen)\n    return () => observer.disconnect()\n  }, [screenWidth])\n  return ref\n}\n\n/**\n * A modern tablet drawn in CSS: aluminum frame with a brushed highlight, thin glass bezel, front camera,\n * status bar and home indicator. Sizes use one unit (`--u`, one percent of the short side), so portrait and\n * landscape share the same proportions. Children render as the screen, laid out at a tablet resolution and scaled to fit.\n */\nfunction TabletFrame({\n  orientation = \"landscape\",\n  variant = \"auto\",\n  src,\n  alt = \"\",\n  videoSrc,\n  poster,\n  screenWidth,\n  statusBar = true,\n  time = \"9:41 AM\",\n  date = \"Thu Oct 1\",\n  homeIndicator = true,\n  screenClassName,\n  className,\n  children,\n  ...props\n}: TabletFrameProps) {\n  const landscape = orientation === \"landscape\"\n  const media = !!(src || videoSrc)\n  const viewportRef = useScreenScale(media ? undefined : (screenWidth ?? undefined))\n  const reduceMotion = !!useReducedMotion()\n\n  return (\n    <div\n      data-slot=\"tablet-frame\"\n      data-variant={variant}\n      data-orientation={orientation}\n      className={cn(\n        \"@container relative w-full select-none\",\n        landscape ? \"aspect-[247.6/178.5] max-w-[640px] [--u:0.7209cqw]\" : \"aspect-[178.5/247.6] max-w-[460px] [--u:1cqw]\",\n        variant === \"silver\" && SILVER,\n        variant === \"black\" && BLACK,\n        variant === \"auto\" && cn(SILVER, BLACK_DARK),\n        className\n      )}\n      {...props}\n    >\n      {/* Side buttons: power on the top edge, volume near the top-right of a landscape tablet. */}\n      {(landscape\n        ? [\"top-[calc(var(--u)*-0.7)] left-[7%] h-[calc(var(--u)*1.1)] w-[4.5%] rounded-t-[0.4cqw]\", \"top-[calc(var(--u)*-0.7)] right-[7%] h-[calc(var(--u)*1.1)] w-[7%] rounded-t-[0.4cqw]\"]\n        : [\"top-[calc(var(--u)*-0.7)] right-[9%] h-[calc(var(--u)*1.1)] w-[8%] rounded-t-[0.4cqw]\", \"right-[calc(var(--u)*-0.7)] top-[9%] h-[8%] w-[calc(var(--u)*1.1)] rounded-r-[0.4cqw]\"]\n      ).map((position) => (\n        <span\n          key={position}\n          aria-hidden=\"true\"\n          className={cn(\"absolute bg-[linear-gradient(90deg,var(--tf-lo),var(--tf-hi)_50%,var(--tf-lo))]\", position)}\n        />\n      ))}\n\n      {/* Aluminum frame */}\n      <div\n        data-slot=\"tablet-frame-body\"\n        className=\"absolute inset-0 rounded-[calc(var(--u)*7.4)] bg-[linear-gradient(135deg,var(--tf-hi),var(--tf-mid)_32%,var(--tf-lo)_58%,var(--tf-mid)_82%,var(--tf-hi))] p-[calc(var(--u)*0.55)] shadow-[inset_0_0_0_0.2cqw_rgb(255_255_255/0.14),0_2.4cqw_6cqw_-1.6cqw_rgb(0_0_0/0.4),0_0.4cqw_1.2cqw_rgb(0_0_0/0.2)]\"\n      >\n        {/* Glass bezel */}\n        <div className=\"relative size-full rounded-[calc(var(--u)*6.9)] bg-black p-[calc(var(--u)*3.1)] shadow-[inset_0_0_0_0.25cqw_rgb(255_255_255/0.07)]\">\n          {/* Front camera, on the long edge */}\n          <span\n            aria-hidden=\"true\"\n            data-slot=\"tablet-frame-camera\"\n            className={cn(\n              \"absolute z-30 size-[calc(var(--u)*1.1)] rounded-full bg-[radial-gradient(circle_at_35%_35%,rgb(90_110_200/0.55),rgb(255_255_255/0.05)_50%,transparent_72%)] ring-[0.18cqw] ring-white/10\",\n              landscape ? \"top-[calc(var(--u)*1)] left-1/2 -translate-x-1/2\" : \"top-1/2 left-[calc(var(--u)*1)] -translate-y-1/2\"\n            )}\n          />\n          <div\n            data-slot=\"tablet-frame-screen\"\n            className={cn(\n              \"relative isolate flex size-full flex-col overflow-hidden rounded-[calc(var(--u)*4.4)] bg-background text-foreground\",\n              screenClassName\n            )}\n          >\n            {media ? (\n              videoSrc ? (\n                <video\n                  data-slot=\"tablet-frame-video\"\n                  className=\"absolute inset-0 size-full object-cover\"\n                  src={videoSrc}\n                  poster={poster}\n                  autoPlay={!reduceMotion}\n                  muted\n                  loop\n                  playsInline\n                />\n              ) : (\n                // eslint-disable-next-line @next/next/no-img-element\n                <Media data-slot=\"tablet-frame-image\" media={src} alt={alt} fill className=\"absolute inset-0\" />\n              )\n            ) : null}\n            {statusBar && (\n              <div\n                data-slot=\"tablet-frame-status-bar\"\n                className=\"relative z-20 flex h-[calc(var(--u)*4.6)] shrink-0 items-center justify-between px-[calc(var(--u)*4.6)] text-[calc(var(--u)*1.9)] font-semibold tracking-tight text-foreground\"\n              >\n                <span className=\"flex gap-[calc(var(--u)*1.4)] tabular-nums\">\n                  <span>{time}</span>\n                  <span className=\"font-medium opacity-80\">{date}</span>\n                </span>\n                <span aria-hidden=\"true\" className=\"flex items-center gap-[calc(var(--u)*1.1)]\">\n                  <svg viewBox=\"0 0 16 12\" className=\"h-[calc(var(--u)*1.8)]\" fill=\"currentColor\">\n                    <path d=\"M8 2.2c2.4 0 4.6.9 6.2 2.5l1.3-1.3A10.6 10.6 0 0 0 8 .4 10.6 10.6 0 0 0 .5 3.4l1.3 1.3A8.8 8.8 0 0 1 8 2.2Z\" />\n                    <path d=\"M8 5.8c1.4 0 2.7.5 3.7 1.4L13 5.9A7 7 0 0 0 8 4a7 7 0 0 0-5 1.9l1.3 1.3c1-.9 2.3-1.4 3.7-1.4Z\" />\n                    <path d=\"M8 9.3c.5 0 1 .2 1.3.5L8 11.6 6.7 9.8c.3-.3.8-.5 1.3-.5Z\" />\n                  </svg>\n                  <span className=\"text-[calc(var(--u)*1.7)] font-medium opacity-80\">100%</span>\n                  <svg viewBox=\"0 0 27 13\" className=\"h-[calc(var(--u)*1.9)]\">\n                    <rect x=\"0.5\" y=\"0.5\" width=\"23\" height=\"12\" rx=\"3.6\" fill=\"none\" stroke=\"currentColor\" strokeOpacity=\"0.4\" />\n                    <rect x=\"2\" y=\"2\" width=\"20\" height=\"9\" rx=\"2.2\" fill=\"currentColor\" />\n                    <path d=\"M25 4.5v4c.8-.3 1.4-1.1 1.4-2s-.6-1.7-1.4-2Z\" fill=\"currentColor\" fillOpacity=\"0.45\" />\n                  </svg>\n                </span>\n              </div>\n            )}\n            {!media && (\n              <div ref={viewportRef} data-slot=\"tablet-frame-content\" className=\"relative min-h-0 flex-1\">\n                {screenWidth ? (\n                  <div\n                    className=\"absolute top-0 left-0 origin-top-left\"\n                    style={{ width: screenWidth, height: \"calc(100% / var(--screen-scale, 1))\", transform: \"scale(var(--screen-scale, 1))\" }}\n                  >\n                    {children}\n                  </div>\n                ) : (\n                  children\n                )}\n              </div>\n            )}\n            {homeIndicator && (\n              <span\n                aria-hidden=\"true\"\n                data-slot=\"tablet-frame-home-indicator\"\n                className=\"pointer-events-none absolute bottom-[calc(var(--u)*1.2)] left-1/2 z-30 h-[calc(var(--u)*0.7)] w-[calc(var(--u)*18)] -translate-x-1/2 rounded-full bg-foreground/85\"\n              />\n            )}\n            {/* Glass reflection */}\n            <span\n              aria-hidden=\"true\"\n              className=\"pointer-events-none absolute inset-0 z-40 bg-[linear-gradient(115deg,rgb(255_255_255/0.07)_0%,rgb(255_255_255/0.015)_34%,transparent_34.2%)]\"\n            />\n          </div>\n        </div>\n      </div>\n    </div>\n  )\n}\n\nexport { TabletFrame, type TabletFrameProps }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/tablet-frame.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "mockup",
      "device",
      "tablet",
      "ipad",
      "screenshot",
      "hero"
    ],
    "version": "1.0.0",
    "updated": "2026-10-01",
    "ai": {
      "summary": "Wrap a tablet screen in <TabletFrame screenWidth={1194}>: children render below a status bar at 1194 CSS px wide (834 in portrait) and are scaled to the frame's width. Set the width with className; the height follows the aspect ratio.",
      "whenToUse": [
        "Showing a dashboard or app on a landing page",
        "Pairing with phone-frame and laptop-frame in a device lineup"
      ],
      "whenNotToUse": [
        "Phone-sized screens (phone-frame, android-frame)",
        "Real screenshots that already include a bezel"
      ],
      "composesWith": [
        "phone-frame",
        "laptop-frame",
        "watch-frame"
      ],
      "a11y": [
        {
          "keys": "Static",
          "action": "Purely visual: the frame adds no focusable elements and hides its decoration from assistive tech"
        },
        {
          "keys": "Reduced motion",
          "action": "Video on the screen is not autoplayed"
        }
      ],
      "customization": [
        "orientation: landscape | portrait",
        "variant: auto | silver | black",
        "screenWidth: virtual width in CSS px",
        "statusBar, time, date, homeIndicator",
        "src/alt or videoSrc/poster instead of children",
        "screenClassName=\"dark\" gives the screen its own theme"
      ]
    },
    "examples": [
      "tablet-frame-demo",
      "tablet-frame-portrait"
    ],
    "url": "https://ui.ballmac.com/components/tablet-frame"
  },
  "categories": [
    "devices"
  ],
  "type": "registry:ui"
}