{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "watch-frame",
  "title": "Watch Frame",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A smartwatch drawn in CSS: rounded aluminum case, digital crown and side button, curved glass and a fading strap in sport, loop or leather. The screen is always dark and children render scaled to fit.",
  "dependencies": [
    "motion@^12"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/media.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/watch-frame.tsx",
      "content": "// Ballmac UI: Watch Frame. https://ui.ballmac.com/components/watch-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 WatchFrameProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  /** Screen content. Ignored when `src` or `videoSrc` is set. Defaults to a dark screen. */\n  children?: React.ReactNode\n  /** Finish of the aluminum case. \"auto\" is silver in light mode and black in dark mode. */\n  variant?: \"auto\" | \"silver\" | \"black\" | \"gold\"\n  /** Strap style and color. */\n  band?: \"sport\" | \"loop\" | \"leather\" | \"none\"\n  /** Strap color, taken from the theme's chart colors. */\n  bandTone?: \"blue\" | \"teal\" | \"orange\" | \"purple\" | \"graphite\" | \"red\"\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 (208 matches a 46 mm watch);\n   * it is then scaled to fit. Omit to lay content out at the frame's actual size.\n   */\n  screenWidth?: number\n  /** Class names for the screen element. */\n  screenClassName?: string\n}\n\nconst SILVER =\n  \"[--wf-hi:color-mix(in_oklch,white_96%,black)] [--wf-mid:color-mix(in_oklch,white_80%,black)] [--wf-lo:color-mix(in_oklch,white_58%,black)]\"\nconst BLACK =\n  \"[--wf-hi:color-mix(in_oklch,white_32%,black)] [--wf-mid:color-mix(in_oklch,white_16%,black)] [--wf-lo:color-mix(in_oklch,white_8%,black)]\"\nconst BLACK_DARK =\n  \"dark:[--wf-hi:color-mix(in_oklch,white_32%,black)] dark:[--wf-mid:color-mix(in_oklch,white_16%,black)] dark:[--wf-lo:color-mix(in_oklch,white_8%,black)]\"\nconst GOLD =\n  \"[--wf-hi:color-mix(in_oklch,var(--chart-3)_35%,white)] [--wf-mid:color-mix(in_oklch,var(--chart-3)_60%,white)] [--wf-lo:color-mix(in_oklch,var(--chart-3)_75%,black)]\"\n\nconst TONES = {\n  blue: \"[--wf-band:color-mix(in_oklch,var(--chart-1)_92%,black)]\",\n  teal: \"[--wf-band:color-mix(in_oklch,var(--chart-2)_92%,black)]\",\n  orange: \"[--wf-band:color-mix(in_oklch,var(--chart-5)_92%,black)]\",\n  purple: \"[--wf-band:color-mix(in_oklch,var(--chart-4)_92%,black)]\",\n  red: \"[--wf-band:color-mix(in_oklch,var(--destructive)_92%,black)]\",\n  graphite: \"[--wf-band:color-mix(in_oklch,white_26%,black)]\",\n}\n\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 smartwatch drawn in CSS: rounded case with a brushed highlight, digital crown, side button, curved glass and a strap\n * that fades out above and below. The screen is always dark. Children render as the screen, laid out at a watch resolution and scaled to fit.\n */\nfunction WatchFrame({\n  variant = \"auto\",\n  band = \"sport\",\n  bandTone = \"graphite\",\n  src,\n  alt = \"\",\n  videoSrc,\n  poster,\n  screenWidth,\n  screenClassName,\n  className,\n  children,\n  ...props\n}: WatchFrameProps) {\n  const media = !!(src || videoSrc)\n  const viewportRef = useScreenScale(media ? undefined : screenWidth)\n  const reduceMotion = !!useReducedMotion()\n  const strap = (side: \"top\" | \"bottom\") => (\n    <span\n      aria-hidden=\"true\"\n      data-slot=\"watch-frame-band\"\n      className={cn(\n        \"absolute left-1/2 h-[34cqw] w-[78cqw] -translate-x-1/2 bg-(--wf-band)\",\n        side === \"top\"\n          ? \"top-0 rounded-t-[8cqw] [mask-image:linear-gradient(to_top,black_55%,transparent)]\"\n          : \"bottom-0 rounded-b-[8cqw] [mask-image:linear-gradient(to_bottom,black_55%,transparent)]\",\n        band === \"sport\" && \"bg-[linear-gradient(90deg,rgb(0_0_0/0.28),transparent_14%,transparent_86%,rgb(0_0_0/0.28)),linear-gradient(var(--wf-band),var(--wf-band))]\",\n        band === \"loop\" &&\n          \"bg-[repeating-linear-gradient(0deg,rgb(255_255_255/0.1)_0_0.5cqw,transparent_0.5cqw_1.4cqw),linear-gradient(90deg,rgb(0_0_0/0.28),transparent_14%,transparent_86%,rgb(0_0_0/0.28)),linear-gradient(var(--wf-band),var(--wf-band))]\",\n        band === \"leather\" &&\n          \"bg-[linear-gradient(90deg,rgb(0_0_0/0.3),transparent_12%,transparent_88%,rgb(0_0_0/0.3)),linear-gradient(color-mix(in_oklch,var(--wf-band)_70%,black),color-mix(in_oklch,var(--wf-band)_70%,black))]\"\n      )}\n    >\n      {band === \"sport\" && side === \"bottom\" && (\n        <>\n          <span className=\"absolute top-[9cqw] left-1/2 size-[2.4cqw] -translate-x-1/2 rounded-full bg-black/35\" />\n          <span className=\"absolute top-[15cqw] left-1/2 size-[2.4cqw] -translate-x-1/2 rounded-full bg-black/35\" />\n          <span className=\"absolute top-[21cqw] left-1/2 size-[2.4cqw] -translate-x-1/2 rounded-full bg-black/35\" />\n        </>\n      )}\n      {band === \"leather\" && <span className=\"absolute inset-y-0 left-[6cqw] border-l-[0.5cqw] border-dashed border-white/30\" />}\n    </span>\n  )\n\n  return (\n    <div\n      data-slot=\"watch-frame\"\n      data-variant={variant}\n      className={cn(\n        \"@container relative aspect-[100/181] w-full max-w-[200px] select-none\",\n        variant === \"silver\" && SILVER,\n        variant === \"black\" && BLACK,\n        variant === \"gold\" && GOLD,\n        variant === \"auto\" && cn(SILVER, BLACK_DARK),\n        TONES[bandTone],\n        className\n      )}\n      {...props}\n    >\n      {band !== \"none\" && (\n        <>\n          {strap(\"top\")}\n          {strap(\"bottom\")}\n        </>\n      )}\n\n      {/* Digital crown and side button */}\n      <span\n        aria-hidden=\"true\"\n        className=\"absolute top-[63cqw] right-[-2.6cqw] h-[16cqw] w-[4.6cqw] rounded-r-[1.8cqw] bg-[repeating-linear-gradient(0deg,var(--wf-hi)_0_0.6cqw,var(--wf-lo)_0.6cqw_1.2cqw)] shadow-[0_0.4cqw_1cqw_rgb(0_0_0/0.3)]\"\n      />\n      <span\n        aria-hidden=\"true\"\n        className=\"absolute top-[88cqw] right-[-1.6cqw] h-[16cqw] w-[2.8cqw] rounded-r-[1.2cqw] bg-[linear-gradient(90deg,var(--wf-lo),var(--wf-hi)_60%,var(--wf-mid))]\"\n      />\n\n      {/* Case */}\n      <div\n        data-slot=\"watch-frame-body\"\n        className=\"absolute top-[30cqw] left-0 h-[121cqw] w-full rounded-[27cqw] bg-[linear-gradient(135deg,var(--wf-hi),var(--wf-mid)_30%,var(--wf-lo)_56%,var(--wf-mid)_80%,var(--wf-hi))] p-[1.6cqw] shadow-[inset_0_0_0_0.3cqw_rgb(255_255_255/0.16),0_3cqw_7cqw_-1.5cqw_rgb(0_0_0/0.45),0_0.6cqw_1.6cqw_rgb(0_0_0/0.25)]\"\n      >\n        <div className=\"size-full rounded-[25.6cqw] bg-black p-[3.2cqw] shadow-[inset_0_0_0_0.4cqw_rgb(255_255_255/0.08)]\">\n          <div\n            data-slot=\"watch-frame-screen\"\n            className={cn(\"dark relative isolate flex size-full flex-col overflow-hidden rounded-[22.4cqw] bg-black text-white\", screenClassName)}\n          >\n            {media ? (\n              videoSrc ? (\n                <video\n                  data-slot=\"watch-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=\"watch-frame-image\" media={src} alt={alt} fill className=\"absolute inset-0\" />\n              )\n            ) : (\n              <div ref={viewportRef} data-slot=\"watch-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            {/* Curved glass */}\n            <span\n              aria-hidden=\"true\"\n              className=\"pointer-events-none absolute inset-0 z-40 rounded-[inherit] bg-[linear-gradient(125deg,rgb(255_255_255/0.14)_0%,rgb(255_255_255/0.02)_32%,transparent_32.2%)] shadow-[inset_0_0_6cqw_rgb(255_255_255/0.05)]\"\n            />\n          </div>\n        </div>\n      </div>\n    </div>\n  )\n}\n\nexport { WatchFrame, type WatchFrameProps }\n",
      "type": "registry:ui",
      "target": "@components/ballmac/watch-frame.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "mockup",
      "device",
      "watch",
      "apple-watch",
      "wearable",
      "hero"
    ],
    "version": "1.0.0",
    "updated": "2026-10-01",
    "ai": {
      "summary": "<WatchFrame screenWidth={208} band=\"sport\" bandTone=\"blue\"> renders children as the watch screen at 208 CSS px wide, scaled to fit. Set the width with className (about w-40).",
      "whenToUse": [
        "Wearable app showcases",
        "Device lineups next to phone-frame"
      ],
      "whenNotToUse": [
        "Large UI that needs a normal screen (phone-frame)"
      ],
      "composesWith": [
        "phone-frame",
        "tablet-frame",
        "progress-ring"
      ],
      "a11y": [
        {
          "keys": "Static",
          "action": "Purely visual: no focusable elements from the frame"
        },
        {
          "keys": "Reduced motion",
          "action": "Video on the screen is not autoplayed"
        }
      ],
      "customization": [
        "variant: auto | silver | black | gold",
        "band: sport | loop | leather | none",
        "bandTone: blue | teal | orange | purple | graphite | red",
        "screenWidth: virtual width in CSS px",
        "src/alt or videoSrc/poster instead of children"
      ]
    },
    "examples": [
      "watch-frame-demo",
      "watch-frame-workout"
    ],
    "url": "https://ui.ballmac.com/components/watch-frame"
  },
  "categories": [
    "devices"
  ],
  "type": "registry:ui"
}