{"$schema":"https://ui.artbloom.tech/schema/registry-item.json","name":"mercury-droplet-choir","type":"registry:ui","title":"Mercury Droplet Choir","description":"Twenty-five chrome droplets breathe in and out of formation, deforming as they travel. Offsets are computed per cell from the centre of the grid.","author":"@artbloom","dependencies":[],"registryDependencies":[],"files":[{"path":"components/ui/mercury-droplet-choir.tsx","target":"components/ui/mercury-droplet-choir.tsx","content":"'use client';\n\nimport type { CSSProperties } from 'react';\n\nimport './mercury-droplet-choir.css';\n\n/** A 5x5 grid: the stylesheet's per-droplet offsets are measured from the centre. */\nconst COLUMNS = 5;\nconst DROPLETS = COLUMNS * COLUMNS;\n\n/** `compact` is the 298x240 catalogue card: the same twenty-five droplets on the same\n *  keyframes, scaled until the whole motion envelope fits 240px of height, with the\n *  editorial slug dropped. Presentation only — see `mercury-droplet-choir.css`. The choir\n *  loops on its own, so the card is alive on load, and nothing in here takes focus or\n *  reads the pointer, so there is no tab order and no touch gesture to hand back. */\nexport type MercuryDropletChoirProps = { compact?: boolean };\n\nexport function MercuryDropletChoir({ compact = false }: MercuryDropletChoirProps) {\n  return (\n    <div\n      className=\"mercury-droplet-choir-stage\"\n      data-compact={compact ? 'true' : undefined}\n    >\n      <div\n        className=\"mercury-droplet-choir\"\n        role=\"img\"\n        aria-label=\"Reflective mercury droplets moving as a synchronized choir\"\n      >\n        <div>\n          {Array.from({ length: DROPLETS }, (_, index) => {\n            const x = index % COLUMNS;\n            const y = Math.floor(index / COLUMNS);\n            const centre = (COLUMNS - 1) / 2;\n            return (\n              <i\n                style={\n                  {\n                    '--i': index,\n                    '--in-x': `${(centre - x) * 18}px`,\n                    '--in-y': `${(centre - y) * 18}px`,\n                    '--out-x': `${(x - centre) * 7}px`,\n                    '--out-y': `${(y - centre) * -11}px`,\n                    '--wave-y': `${(centre - x) * 10}px`,\n                  } as CSSProperties\n                }\n                key={index}\n              />\n            );\n          })}\n        </div>\n        <b>CHOIR</b>\n        <span>COLLECTIVE MASS / 025</span>\n      </div>\n    </div>\n  );\n}\n","type":"registry:ui"},{"path":"components/ui/mercury-droplet-choir.css","target":"components/ui/mercury-droplet-choir.css","content":"@keyframes droplet-choir {\n  0%,\n  100% {\n    transform: translate3d(0, 0, 0) scale(0.72);\n    border-radius: 50%;\n  }\n  23% {\n    transform: translate3d(var(--in-x), var(--in-y), 0) scale(1.18);\n  }\n  48% {\n    transform: translate3d(var(--out-x), var(--out-y), 0) scale(0.86);\n    border-radius: 38% 62% 44% 56%;\n  }\n  72% {\n    transform: translate3d(0, var(--wave-y), 0) scale(1);\n  }\n}\n\n/* The surface the choir is designed to sit on. Size it from the parent. */\n.mercury-droplet-choir-stage {\n  position: relative;\n  display: grid;\n  place-items: center;\n  width: 100%;\n  height: 100%;\n  min-height: 240px;\n  container-type: inline-size;\n  overflow: hidden;\n  background: #151617;\n  color: #e7e5de;\n}\n\n.mercury-droplet-choir {\n  position: relative;\n  width: 100%;\n  height: 100%;\n  display: grid;\n  place-items: center;\n  overflow: hidden;\n  background: radial-gradient(circle at 50% 46%, #303337 0, #151617 44%, #090a0a 78%);\n}\n\n.mercury-droplet-choir > div {\n  position: relative;\n  width: 310px;\n  height: 310px;\n  display: grid;\n  grid-template-columns: repeat(5, 1fr);\n  gap: 17px;\n  filter: contrast(1.12);\n}\n\n.mercury-droplet-choir i {\n  display: block;\n  border-radius: 50%;\n  background: radial-gradient(\n    circle at 31% 25%,\n    #fff 0 4%,\n    #bbc0c3 7%,\n    #494d50 18%,\n    #090a0b 53%,\n    #8d9295 75%,\n    #111 88%\n  );\n  box-shadow:\n    inset -7px -9px 12px #000,\n    0 12px 16px rgba(0, 0, 0, 0.46);\n  animation: droplet-choir 5.8s calc(var(--i) * -90ms) cubic-bezier(0.6, 0, 0.25, 1)\n    infinite;\n  will-change: transform;\n}\n\n.mercury-droplet-choir i:nth-child(4n) {\n  border-radius: 62% 38% 55% 45%;\n}\n\n.mercury-droplet-choir > b {\n  position: absolute;\n  font-size: 74px;\n  letter-spacing: -0.08em;\n  color: #f1eee6;\n  mix-blend-mode: difference;\n  z-index: 2;\n}\n\n.mercury-droplet-choir > span {\n  position: absolute;\n  left: 18px;\n  bottom: 16px;\n  font: 8px ui-monospace, SFMono-Regular, Menlo, monospace;\n  letter-spacing: 0.13em;\n}\n\n@container (max-width: 520px) {\n  .mercury-droplet-choir > div {\n    scale: 0.75;\n  }\n}\n\n/*\n * The card variant: the 298x240 catalogue frame, at that real size and never scaled.\n * There is no marketing section to dismantle here — the stage is already nothing but the\n * choir, a wordmark and an 8px slug — so the whole job is arithmetic: get the motion,\n * not the grid box, inside 240px of height, and stop the wordmark from sitting across the\n * mechanism.\n */\n.mercury-droplet-choir-stage[data-compact='true'] {\n  /* 240px happens to match the frame exactly, but a min-height outranks `height: 100%`\n     the moment the frame is a pixel shorter, so it goes to zero. The frame does the\n     rounding and the clipping, and this stage never set a radius of its own. */\n  min-height: 0;\n  height: 100%;\n}\n\n/*\n * No `touch-action` line, and none needed: nothing in this component reads the pointer —\n * the droplets are CSS keyframes and the stage has no handlers — so it never becomes a\n * drag surface that claims the vertical gesture. A touch on the card scrolls the page as\n * it should, and declaring `pan-y` would only cost the card its pinch-zoom.\n */\n\n/*\n * 0.66, and it is the height that sets it. The grid is a 310px box of 48.4px cells on a\n * 65.4px pitch, but the motion reaches well past that box: at 48% the blob throws the\n * outer columns 10.6px clear either side, and at 72% the wave lifts one column 20px above\n * the top edge and drops another 20px below the bottom. So the thing that has to fit is\n * 331x350, not 310x310 — and 350 x 0.66 = 231px, which leaves 4.5px of clearance top and\n * bottom inside 240 with none of the choreography clipped. The narrow-container rule above\n * lands on 0.75, which is right for a 520px-wide surface and 11px per edge too tall here.\n *\n * A scale rather than a smaller box, because every offset in the component is a pixel\n * constant read against that 65.4px pitch: 18px in, 7px and 11px out, 10px of wave.\n * Shrinking the cells while those stayed fixed would turn the converge phase from a crowd\n * into a pile-up, and it would leave the `inset -7px -9px 12px` highlight sized for a 48px\n * ball on a 32px one — which reads as a black disc, not as chrome.\n *\n * 0.66 is sized to the 240px frame this composition is authored for, and 240 is the floor:\n * `ItemCard` and `ItemGrid` both default `height` to it, and every surface that shows a\n * card — the catalogue grids, a profile, bookmarks, the related row under an item — goes\n * through one of the two. The landing lists ask for 260, which is taller and fits. So the\n * 231px envelope is never cropped on a card.\n *\n * This paragraph used to say three of those surfaces passed `height={200}`, and worked out\n * that 200 would cut the ball the wave lifts out of the top row roughly in half. No surface\n * passes 200. The conclusion is unchanged — 0.66, sized for 240 — but it now holds because\n * nothing is shorter than 240 rather than in spite of something that was.\n *\n * A height-driven scale is not available either way: the stage is an `inline-size`\n * container, `cqh` needs `container-type: size`, and `scale` takes a number rather than a\n * length.\n */\n.mercury-droplet-choir-stage[data-compact='true'] .mercury-droplet-choir > div {\n  scale: 0.66;\n}\n\n/*\n * A cqw clamp instead of the shipped 74px, resolved against the stage — it is the\n * `inline-size` container, so 13cqw is 38.7px across a 298px card. CHOIR carries -0.08em\n * of tracking and sets around 2.6em, so that is roughly 100px of word over the 205px grid;\n * at 74px it runs nearer 195px and covers the choir end to end, which is the mechanism\n * losing to its own label.\n *\n * The ceiling is 40px and not the shipped 74px, because the choir does not grow with the\n * frame: the scale above is a constant, so the grid renders 205px wide in a 298px card\n * and 205px wide in the 607px one `grid-cols-1` produces at a 639px viewport. 13cqw\n * reaches 74px at 569px of container, inside that range, and would hand back exactly the\n * end-to-end wordmark this rule exists to remove. Pinned at 40px the mark stays about\n * half the choir at every card width the catalogue renders, and the 22px floor holds a\n * frame narrower than this one.\n */\n.mercury-droplet-choir-stage[data-compact='true'] .mercury-droplet-choir > b {\n  font-size: clamp(22px, 13cqw, 40px);\n}\n\n/* The slug is editorial detail for a full-width section. On a card the item's own title\n   and description say what this is, and 8px of tracking under the droplets is noise\n   competing for the only 240px there are. */\n.mercury-droplet-choir-stage[data-compact='true'] .mercury-droplet-choir > span {\n  display: none;\n}\n\n/* The loop rests as a row of identical 0.72-scale circles, which is the least interesting\n   frame it has. The 72% pose is the one worth keeping: full scale, each droplet lifted by\n   its own `--wave-y`, so the row still reads as a choir rather than a ruler. The radius is\n   pinned back to a circle because 72% inherits a mid-morph value from 48%. */\n@media (prefers-reduced-motion: reduce) {\n  .mercury-droplet-choir i {\n    animation: none;\n    transform: translate3d(0, var(--wave-y), 0) scale(1);\n    border-radius: 50%;\n  }\n}\n","type":"registry:file"}],"meta":{"kind":"animations","categories":["morph","loaders","infinite"],"docs":"https://ui.artbloom.tech/artbloom/animations/mercury-droplet-choir"}}