{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "phone-frame",
  "title": "Phone Frame",
  "description": "A modern phone drawn in CSS with a titanium band, side buttons, Dynamic Island, status bar and home indicator. Children render as the screen, laid out at phone resolution and scaled to fit.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "@tealmac/media"
  ],
  "files": [
    {
      "path": "registry/tealmac/phone-frame.tsx",
      "content": "// Adapted from Ballmac UI (https://github.com/vamsiy78/ballmac-ui), MIT License, Copyright (c) 2026 Ballmac.\n\"use client\"\n\nimport * as React from \"react\"\nimport { useReducedMotion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Media } from \"@/registry/tealmac/media\"\n\ntype PhoneFrameProps = 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  /** Finish of the frame. \"auto\" is natural titanium in light mode and black titanium in dark mode. */\n  variant?: \"auto\" | \"natural\" | \"black\"\n  /** Image shown on the screen, cropped to fill it (status bar and island stay on top). */\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 (393 matches a 6.1\" phone); it is\n   * 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, signal, Wi-Fi, battery). */\n  statusBar?: boolean\n  /** Time shown in the status bar. */\n  time?: 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 NATURAL =\n  \"[--pf-hi:color-mix(in_oklch,white_92%,black)] [--pf-mid:color-mix(in_oklch,white_74%,black)] [--pf-lo:color-mix(in_oklch,white_56%,black)] [--pf-btn:color-mix(in_oklch,white_66%,black)]\"\nconst BLACK =\n  \"[--pf-hi:color-mix(in_oklch,white_42%,black)] [--pf-mid:color-mix(in_oklch,white_20%,black)] [--pf-lo:color-mix(in_oklch,white_10%,black)] [--pf-btn:color-mix(in_oklch,white_24%,black)]\"\nconst BLACK_DARK =\n  \"dark:[--pf-hi:color-mix(in_oklch,white_42%,black)] dark:[--pf-mid:color-mix(in_oklch,white_20%,black)] dark:[--pf-lo:color-mix(in_oklch,white_10%,black)] dark:[--pf-btn:color-mix(in_oklch,white_24%,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 = () =>\n      screen.style.setProperty(\n        \"--screen-scale\",\n        String(screen.clientWidth / screenWidth)\n      )\n    measure()\n    const observer = new ResizeObserver(measure)\n    observer.observe(screen)\n    return () => observer.disconnect()\n  }, [screenWidth])\n  return ref\n}\n\nfunction StatusBar({ time }: { time: string }) {\n  return (\n    <div\n      data-slot=\"phone-frame-status-bar\"\n      className=\"text-foreground relative z-20 flex h-[13cqw] shrink-0 items-center justify-between px-[8.5cqw] pt-[1.2cqw] text-[4.1cqw] font-semibold tracking-tight\"\n    >\n      <span className=\"w-[20cqw] text-center tabular-nums\">{time}</span>\n      <span aria-hidden=\"true\" className=\"flex items-center gap-[1.4cqw]\">\n        {/* Cellular */}\n        <svg viewBox=\"0 0 18 12\" className=\"h-[2.9cqw]\" fill=\"currentColor\">\n          <rect x=\"0\" y=\"8\" width=\"3\" height=\"4\" rx=\"0.8\" />\n          <rect x=\"5\" y=\"5.5\" width=\"3\" height=\"6.5\" rx=\"0.8\" />\n          <rect x=\"10\" y=\"3\" width=\"3\" height=\"9\" rx=\"0.8\" />\n          <rect x=\"15\" y=\"0\" width=\"3\" height=\"12\" rx=\"0.8\" />\n        </svg>\n        {/* Wi-Fi */}\n        <svg viewBox=\"0 0 16 12\" className=\"h-[2.9cqw]\" 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        {/* Battery */}\n        <svg viewBox=\"0 0 27 13\" className=\"h-[3.1cqw]\">\n          <rect\n            x=\"0.5\"\n            y=\"0.5\"\n            width=\"23\"\n            height=\"12\"\n            rx=\"3.6\"\n            fill=\"none\"\n            stroke=\"currentColor\"\n            strokeOpacity=\"0.4\"\n          />\n          <rect\n            x=\"2\"\n            y=\"2\"\n            width=\"17\"\n            height=\"9\"\n            rx=\"2.2\"\n            fill=\"currentColor\"\n          />\n          <path\n            d=\"M25 4.5v4c.8-.3 1.4-1.1 1.4-2s-.6-1.7-1.4-2Z\"\n            fill=\"currentColor\"\n            fillOpacity=\"0.45\"\n          />\n        </svg>\n      </span>\n    </div>\n  )\n}\n\nfunction PhoneFrame({\n  variant = \"auto\",\n  src,\n  alt = \"\",\n  videoSrc,\n  poster,\n  screenWidth,\n  statusBar = true,\n  time = \"9:41\",\n  homeIndicator = true,\n  screenClassName,\n  className,\n  children,\n  ...props\n}: PhoneFrameProps) {\n  const media = !!(src || videoSrc)\n  const viewportRef = useScreenScale(media ? undefined : screenWidth)\n  const reduceMotion = !!useReducedMotion()\n\n  return (\n    <div\n      data-slot=\"phone-frame\"\n      data-variant={variant}\n      className={cn(\n        \"@container relative aspect-[71.6/146.6] w-full max-w-[320px] select-none\",\n        variant === \"natural\" && NATURAL,\n        variant === \"black\" && BLACK,\n        variant === \"auto\" && cn(NATURAL, BLACK_DARK),\n        className\n      )}\n      {...props}\n    >\n      {/* Buttons: action and volume on the left, side button on the right. */}\n      {[\n        \"left-[-0.9cqw] top-[18%] h-[5.2%] rounded-l-[0.6cqw]\",\n        \"left-[-0.9cqw] top-[26.5%] h-[8.6%] rounded-l-[0.6cqw]\",\n        \"left-[-0.9cqw] top-[37%] h-[8.6%] rounded-l-[0.6cqw]\",\n        \"right-[-0.9cqw] top-[29%] h-[13.5%] rounded-r-[0.6cqw]\",\n      ].map((position) => (\n        <span\n          key={position}\n          aria-hidden=\"true\"\n          className={cn(\n            \"absolute w-[1.4cqw] bg-[linear-gradient(90deg,var(--pf-lo),var(--pf-btn)_40%,var(--pf-hi)_55%,var(--pf-lo))]\",\n            position\n          )}\n        />\n      ))}\n\n      {/* Titanium band */}\n      <div\n        data-slot=\"phone-frame-body\"\n        className=\"absolute inset-0 rounded-[16.5cqw] bg-[linear-gradient(135deg,var(--pf-hi),var(--pf-mid)_30%,var(--pf-lo)_55%,var(--pf-mid)_80%,var(--pf-hi))] p-[1.25cqw] shadow-[inset_0_0_0_0.25cqw_rgb(255_255_255/0.12),0_2cqw_6cqw_-1cqw_rgb(0_0_0/0.35),0_0.5cqw_1.2cqw_rgb(0_0_0/0.18)]\"\n      >\n        {/* Black border glass */}\n        <div className=\"size-full rounded-[15.3cqw] bg-black p-[3.1cqw] shadow-[inset_0_0_0_0.35cqw_rgb(255_255_255/0.07)]\">\n          <div\n            data-slot=\"phone-frame-screen\"\n            className={cn(\n              \"bg-background text-foreground relative isolate flex size-full flex-col overflow-hidden rounded-[12.4cqw]\",\n              screenClassName\n            )}\n          >\n            {media ? (\n              videoSrc ? (\n                <video\n                  data-slot=\"phone-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                <Media\n                  data-slot=\"phone-frame-image\"\n                  media={src}\n                  alt={alt}\n                  fill\n                  className=\"absolute inset-0\"\n                />\n              )\n            ) : null}\n            {statusBar && <StatusBar time={time} />}\n            {!media && (\n              <div\n                ref={viewportRef}\n                data-slot=\"phone-frame-content\"\n                className=\"relative min-h-0 flex-1\"\n              >\n                {screenWidth ? (\n                  <div\n                    className=\"absolute top-0 left-0 origin-top-left\"\n                    style={{\n                      width: screenWidth,\n                      height: \"calc(100% / var(--screen-scale, 1))\",\n                      transform: \"scale(var(--screen-scale, 1))\",\n                    }}\n                  >\n                    {children}\n                  </div>\n                ) : (\n                  children\n                )}\n              </div>\n            )}\n            {/* Dynamic Island */}\n            <span\n              aria-hidden=\"true\"\n              data-slot=\"phone-frame-island\"\n              className=\"pointer-events-none absolute top-[2.6cqw] left-1/2 z-30 flex h-[8.8cqw] w-[30cqw] -translate-x-1/2 items-center justify-end rounded-full bg-black pr-[2.6cqw]\"\n            >\n              <span className=\"size-[3cqw] rounded-full bg-[radial-gradient(circle_at_35%_35%,rgb(90_110_200/0.6),rgb(255_255_255/0.05)_50%,transparent_72%)] ring-[0.3cqw] ring-white/5\" />\n            </span>\n            {homeIndicator && (\n              <span\n                aria-hidden=\"true\"\n                data-slot=\"phone-frame-home-indicator\"\n                className=\"bg-foreground/85 pointer-events-none absolute bottom-[2.2cqw] left-1/2 z-30 h-[1.35cqw] w-[36cqw] -translate-x-1/2 rounded-full\"\n              />\n            )}\n            {/* Glass reflection */}\n            <span\n              aria-hidden=\"true\"\n              className=\"pointer-events-none absolute inset-0 z-40 bg-[linear-gradient(120deg,rgb(255_255_255/0.08)_0%,rgb(255_255_255/0.02)_36%,transparent_36.2%)]\"\n            />\n          </div>\n        </div>\n      </div>\n    </div>\n  )\n}\n\nexport { PhoneFrame, type PhoneFrameProps }\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}