{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "devices-1",
  "title": "Devices 1: Mac, iPad, iPhone and Watch lineup",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A lineup of laptop, tablet, phone and watch frames with their own token-drawn screens, scaled as one picture. Buttons underneath bring a device forward and dim the rest.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/laptop-frame.json",
    "https://ui.ballmac.com/r/phone-frame.json",
    "https://ui.ballmac.com/r/tablet-frame.json",
    "https://ui.ballmac.com/r/watch-frame.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/devices-1/devices-1.tsx",
      "content": "// Ballmac UI: Devices 1. https://ui.ballmac.com/blocks/devices-1\n\"use client\"\n\nimport * as React from \"react\"\nimport { Bell, Home, Laptop, PieChart, Receipt, Smartphone, Tablet, Users, Watch } from \"lucide-react\"\n\nimport { LaptopFrame } from \"@/components/ballmac/laptop-frame\"\nimport { PhoneFrame } from \"@/components/ballmac/phone-frame\"\nimport { TabletFrame } from \"@/components/ballmac/tablet-frame\"\nimport { WatchFrame } from \"@/components/ballmac/watch-frame\"\nimport { cn } from \"@/lib/utils\"\n\ntype Devices1Device = \"mac\" | \"ipad\" | \"iphone\" | \"watch\"\n\ntype Devices1Props = Omit<React.ComponentProps<\"section\">, \"title\"> & {\n  /** Label above the heading. */\n  eyebrow?: string\n  /** Section heading. */\n  title?: string\n  /** One or two sentences under the heading. */\n  description?: string\n  /** Names and one-line descriptions for each device, shown as the buttons under the picture. */\n  captions?: Partial<Record<Devices1Device, { title: string; description: string }>>\n  /** Replaces a device's sample screen. Each screen is laid out at the real device width: 1280, 834, 393 and 208 CSS pixels. */\n  screens?: Partial<Record<Devices1Device, React.ReactNode>>\n  /** Real screenshots, one per device: `{ src, alt }`. The frame crops each to its screen. Wins over `screens`. */\n  screenshots?: Partial<Record<Devices1Device, { src: string; alt: string }>>\n}\n\nconst defaultCaptions: Record<Devices1Device, { title: string; description: string }> = {\n  mac: { title: \"Mac\", description: \"The full workspace, with keyboard shortcuts and a menu bar companion.\" },\n  ipad: { title: \"iPad\", description: \"Review and approve on the couch, with Pencil markup on receipts.\" },\n  iphone: { title: \"iPhone\", description: \"Snap a receipt, approve a payment, check cash from anywhere.\" },\n  watch: { title: \"Apple Watch\", description: \"A glance at what’s been paid today, with a tap to nudge late invoices.\" },\n}\n\nconst shot = (s?: { src: string; alt: string }) => (s ? { src: s.src, alt: s.alt } : {})\n\nconst icons = { mac: Laptop, ipad: Tablet, iphone: Smartphone, watch: Watch }\n\nfunction MacScreen() {\n  return (\n    <div className=\"bg-background flex h-full w-[1280px] text-[15px]\">\n      <aside className=\"bg-muted/40 w-[220px] shrink-0 space-y-1 border-e p-4\">\n        <div className=\"mb-4 flex items-center gap-2.5 px-2\"><span className=\"bg-foreground text-background flex size-7 items-center justify-center rounded-lg text-sm font-bold\">L</span><span className=\"text-base font-semibold\">Ledger</span></div>\n        {[[\"Overview\", Home], [\"Invoices\", Receipt], [\"Customers\", Users], [\"Reports\", PieChart]].map(([l, I], i) => {\n          const Icon = I as typeof Home\n          return <span key={String(l)} className={`flex items-center gap-3 rounded-lg px-3 py-2.5 font-medium ${i === 0 ? \"bg-background shadow-sm ring-1 ring-border\" : \"text-muted-foreground\"}`}><Icon className=\"size-[18px]\" />{String(l)}</span>\n        })}\n      </aside>\n      <div className=\"min-w-0 flex-1 space-y-5 p-8\">\n        <p className=\"text-2xl font-semibold tracking-tight\">Overview</p>\n        <div className=\"grid grid-cols-4 gap-4\">\n          {[[\"Revenue\", \"$128,430\"], [\"Customers\", \"2,847\"], [\"Avg. invoice\", \"$1,260\"], [\"Paid on time\", \"94.2%\"]].map(([k, v]) => (\n            <div key={k} className=\"bg-card rounded-xl border p-4\"><p className=\"text-muted-foreground text-sm\">{k}</p><p className=\"mt-2 text-[26px] font-semibold tracking-tight\">{v}</p></div>\n          ))}\n        </div>\n        <div className=\"bg-card rounded-xl border p-5\">\n          <p className=\"font-medium\">Revenue</p>\n          <svg viewBox=\"0 0 600 200\" preserveAspectRatio=\"none\" className=\"mt-3 h-[260px] w-full\" aria-hidden=\"true\">\n            <defs><linearGradient id=\"d1-mac\" x1=\"0\" x2=\"0\" y1=\"0\" y2=\"1\"><stop offset=\"0%\" style={{ stopColor: \"var(--chart-1)\", stopOpacity: 0.35 }} /><stop offset=\"100%\" style={{ stopColor: \"var(--chart-1)\", stopOpacity: 0 }} /></linearGradient></defs>\n            <path d=\"M0 150 C60 140 90 110 150 120 S250 155 310 100 S420 70 470 55 S560 45 600 30 L600 200 L0 200Z\" fill=\"url(#d1-mac)\" />\n            <path d=\"M0 150 C60 140 90 110 150 120 S250 155 310 100 S420 70 470 55 S560 45 600 30\" fill=\"none\" stroke=\"var(--chart-1)\" strokeWidth=\"2.5\" strokeLinecap=\"round\" />\n          </svg>\n        </div>\n      </div>\n    </div>\n  )\n}\n\nfunction IpadScreen() {\n  return (\n    <div className=\"bg-background h-full w-[834px] p-8 text-[17px]\">\n      <p className=\"text-3xl font-semibold tracking-tight\">Approvals</p>\n      <p className=\"text-muted-foreground mt-1\">4 waiting for you</p>\n      <ul className=\"mt-6 divide-y rounded-2xl border\">\n        {[[\"Northwind Studio\", \"Design retainer\", \"$4,200\"], [\"Globex Corp\", \"Annual license\", \"$12,800\"], [\"Initech\", \"Support plan\", \"$960\"], [\"Umbrella Labs\", \"Hardware\", \"$3,450\"]].map(([a, b, c]) => (\n          <li key={a} className=\"flex items-center gap-4 px-5 py-4\"><span className=\"bg-chart-1/20 flex size-11 items-center justify-center rounded-xl font-semibold\">{a[0]}</span><span className=\"flex-1\"><span className=\"block font-medium\">{a}</span><span className=\"text-muted-foreground block text-[15px]\">{b}</span></span><span className=\"font-semibold tabular-nums\">{c}</span></li>\n        ))}\n      </ul>\n      <div className=\"mt-6 flex gap-3\"><span className=\"bg-foreground text-background rounded-full px-6 py-3 font-medium\">Approve all</span><span className=\"rounded-full border px-6 py-3 font-medium\">Review</span></div>\n    </div>\n  )\n}\n\nfunction PhoneScreen() {\n  return (\n    <div className=\"bg-background flex h-full w-[393px] flex-col px-5 pt-14 text-[16px]\">\n      <p className=\"text-muted-foreground text-sm\">Cash on hand</p>\n      <p className=\"text-[40px] font-semibold tracking-tight tabular-nums\">$84,210</p>\n      <p className=\"text-chart-2 text-sm font-medium\">+$4,200 today</p>\n      <ul className=\"mt-5 divide-y rounded-2xl border\">\n        {[[\"Northwind\", \"Paid\", \"$4,200\"], [\"Globex\", \"Due\", \"$12,800\"], [\"Initech\", \"Paid\", \"$960\"]].map(([a, s, v]) => (\n          <li key={a} className=\"flex items-center gap-3 px-4 py-3.5\"><span className=\"flex-1 font-medium\">{a}</span><span className={`rounded-full px-2 py-0.5 text-xs ${s === \"Paid\" ? \"bg-chart-2/15\" : \"bg-chart-3/20\"}`}>{s}</span><span className=\"w-16 text-end tabular-nums\">{v}</span></li>\n        ))}\n      </ul>\n      <div className=\"mt-auto -mx-5 flex justify-around border-t px-4 pt-3 pb-8 text-xs\">\n        {[[Home, \"Home\"], [Receipt, \"Invoices\"], [Bell, \"Alerts\"], [Users, \"People\"]].map(([I, l], i) => { const Icon = I as typeof Home; return <span key={String(l)} className={`flex flex-col items-center gap-1 ${i === 0 ? \"\" : \"text-muted-foreground\"}`}><Icon className=\"size-5\" />{String(l)}</span> })}\n      </div>\n    </div>\n  )\n}\n\nfunction WatchScreen() {\n  return (\n    <div className=\"flex h-full w-[208px] flex-col items-center justify-center bg-black text-white\">\n      <div className=\"relative flex size-28 items-center justify-center\">\n        <svg viewBox=\"0 0 100 100\" className=\"absolute inset-0 -rotate-90\" aria-hidden=\"true\"><circle cx=\"50\" cy=\"50\" r=\"42\" fill=\"none\" stroke=\"white\" strokeOpacity=\"0.18\" strokeWidth=\"9\" /><circle cx=\"50\" cy=\"50\" r=\"42\" fill=\"none\" stroke=\"var(--chart-2)\" strokeWidth=\"9\" strokeLinecap=\"round\" strokeDasharray=\"264\" strokeDashoffset=\"70\" /></svg>\n        <div className=\"text-center\"><p className=\"text-[22px] leading-none font-semibold tabular-nums\">$4.2k</p><p className=\"mt-1 text-[11px] text-white/70\">paid today</p></div>\n      </div>\n      <p className=\"mt-3 text-[12px] text-white/70\">2 invoices due</p>\n    </div>\n  )\n}\n\nfunction Devices1({\n  eyebrow = \"Everywhere you work\",\n  title = \"One app, every screen.\",\n  description = \"Start on your Mac, approve on your iPad, check in from your iPhone and glance at your wrist. Everything stays in sync.\",\n  captions,\n  screens,\n  screenshots,\n  className,\n  ...props\n}: Devices1Props) {\n  const [pinned, setPinned] = React.useState<Devices1Device | null>(null)\n  const [hovered, setHovered] = React.useState<Devices1Device | null>(null)\n  const focus = hovered ?? pinned\n  const text = { ...defaultCaptions, ...captions }\n  const order: Devices1Device[] = [\"mac\", \"ipad\", \"iphone\", \"watch\"]\n  const dim = (d: Devices1Device) => focus !== null && focus !== d\n  const lift = (d: Devices1Device) => focus === d\n\n  return (\n    <section data-slot=\"devices-1\" className={cn(\"mx-auto max-w-6xl px-4 py-20 sm:px-6 md:py-28\", className)} {...props}>\n      <div className=\"mx-auto max-w-2xl text-center\">\n        <p className=\"text-muted-foreground text-sm font-medium\">{eyebrow}</p>\n        <h2 className=\"mt-3 text-3xl font-semibold tracking-[-0.035em] text-balance sm:text-4xl lg:text-5xl\">{title}</h2>\n        <p className=\"text-muted-foreground mt-4 text-lg text-pretty\">{description}</p>\n      </div>\n\n      {/* The picture is decorative; the buttons below say the same thing in words. */}\n      <div aria-hidden=\"true\" className=\"@container mt-14 md:mt-20\">\n        <div className=\"relative aspect-[2/1] sm:aspect-[2.5/1] w-full\">\n          <div className=\"bg-chart-1/15 absolute inset-x-[8%] bottom-0 h-1/2 rounded-[50%] blur-3xl\" />\n          {[\n            { d: \"ipad\" as const, pos: \"start-[1%] bottom-[2%] w-[20cqw] z-0\", node: <TabletFrame orientation=\"portrait\" screenWidth={834} {...shot(screenshots?.ipad)}>{screenshots?.ipad ? null : (screens?.ipad ?? <IpadScreen />)}</TabletFrame> },\n            { d: \"mac\" as const, pos: \"start-[18%] bottom-0 w-[62cqw] z-10\", node: <LaptopFrame screenWidth={1280} {...shot(screenshots?.mac)}>{screenshots?.mac ? null : (screens?.mac ?? <MacScreen />)}</LaptopFrame> },\n            { d: \"iphone\" as const, pos: \"end-[6%] bottom-0 w-[14cqw] z-20\", node: <PhoneFrame screenWidth={393} {...shot(screenshots?.iphone)}>{screenshots?.iphone ? null : (screens?.iphone ?? <PhoneScreen />)}</PhoneFrame> },\n            { d: \"watch\" as const, pos: \"end-[0.5%] bottom-[3%] w-[7.5cqw] z-20\", node: <WatchFrame screenWidth={208} band=\"sport\" bandTone=\"blue\" variant=\"black\" {...shot(screenshots?.watch)}>{screenshots?.watch ? null : (screens?.watch ?? <WatchScreen />)}</WatchFrame> },\n          ].map(({ d, pos, node }) => (\n            <div key={d} className={cn(\"absolute origin-bottom transition-[transform,opacity,filter] duration-500 ease-out motion-reduce:transition-none\", pos, dim(d) && \"opacity-40 blur-[2px] saturate-50\", lift(d) && \"-translate-y-[3%] scale-[1.04]\")}>{node}</div>\n          ))}\n        </div>\n      </div>\n\n      <div role=\"group\" aria-label=\"Choose a device to highlight\" className=\"mt-10 grid gap-3 sm:grid-cols-2 lg:grid-cols-4\">\n        {order.map((d) => {\n          const Icon = icons[d]\n          const on = pinned === d\n          const lit = focus === d\n          return (\n            <button\n              key={d}\n              type=\"button\"\n              aria-pressed={on}\n              onClick={() => setPinned(on ? null : d)}\n              onMouseEnter={() => setHovered(d)}\n              onMouseLeave={() => setHovered(null)}\n              className={cn(\"focus-visible:ring-ring/50 rounded-2xl border p-4 text-start outline-none transition-colors focus-visible:ring-[3px]\", lit ? \"border-foreground bg-accent/50\" : \"hover:bg-accent/30\")}\n            >\n              <span className=\"flex items-center gap-2 font-semibold\"><Icon className=\"size-4\" aria-hidden=\"true\" />{text[d].title}</span>\n              <span className=\"text-muted-foreground mt-1.5 block text-sm text-pretty\">{text[d].description}</span>\n            </button>\n          )\n        })}\n      </div>\n    </section>\n  )\n}\n\nexport { Devices1, type Devices1Props, type Devices1Device }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/devices-1/devices-1.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "devices",
      "mac",
      "ipad",
      "iphone",
      "watch",
      "multi-platform",
      "lineup"
    ],
    "version": "1.0.0",
    "updated": "2026-10-01",
    "ai": {
      "summary": "A multi-platform section. Edit eyebrow, title, description and captions; pass screens={{ mac, ipad, iphone, watch }} to replace the sample screens, each laid out at its real device width (1280, 834, 393, 208).",
      "whenToUse": [
        "Apps that ship on several Apple platforms",
        "Showing responsive or cross-device behaviour"
      ],
      "whenNotToUse": [
        "Single-platform products (use hero-5 or showcase-1)"
      ],
      "composesWith": [
        "hero-5",
        "features-4",
        "download-1",
        "pricing-4"
      ],
      "a11y": [
        {
          "keys": "Tab / Enter on a device button",
          "action": "Brings that device forward (aria-pressed); pressing again clears it"
        },
        {
          "keys": "Picture",
          "action": "Hidden from assistive technology; the four buttons describe each device in words"
        },
        {
          "keys": "Reduced motion",
          "action": "Devices change focus without moving"
        }
      ],
      "customization": [
        "captions per device",
        "screens: any node per device"
      ]
    },
    "examples": [
      "devices-1-demo",
      "devices-1-custom"
    ],
    "url": "https://ui.ballmac.com/components/devices-1"
  },
  "categories": [
    "blocks",
    "devices"
  ],
  "type": "registry:block"
}