{"$schema":"https://ui.artbloom.tech/schema/registry-item.json","name":"domino-word","type":"registry:ui","title":"Domino Word","description":"Letter tiles fall into each other in sequence, each with a cast side and a real ground shadow. Any word, one tile per letter.","author":"@artbloom","dependencies":[],"registryDependencies":[],"files":[{"path":"components/ui/domino-word.tsx","target":"components/ui/domino-word.tsx","content":"'use client';\n\nimport type { CSSProperties } from 'react';\n\nimport './domino-word.css';\n\nexport type DominoWordProps = {\n  /** Each letter becomes a tile, so short words read best. */\n  word?: string;\n  /**\n   * The 298x240 catalogue card: the same rack and the same fall, handed the whole\n   * frame. Presentation only — see `domino-word.css`. There is no tab order to\n   * withdraw under the card's `aria-hidden`, because the rack is a `role=\"img\"`\n   * div of spans and nothing in it was ever focusable or pointer-driven.\n   */\n  compact?: boolean;\n};\n\nexport function DominoWord({ word = 'DOMINO', compact = false }: DominoWordProps = {}) {\n  const letters = [...word];\n  return (\n    <div className=\"domino-word-stage\" data-compact={compact ? 'true' : undefined}>\n      <div\n        className=\"domino-word\"\n        role=\"img\"\n        aria-label={`${word} letter tiles falling like dominoes`}\n        // The stylesheet sizes a tile from the stage width divided by the letter\n        // count, so a long word narrows the rack instead of overflowing it.\n        style={{ '--n': letters.length } as CSSProperties}\n      >\n        {letters.map((letter, index) => (\n          <span style={{ '--i': index } as CSSProperties} key={`${letter}-${index}`}>\n            <b>{letter}</b>\n            <i />\n          </span>\n        ))}\n      </div>\n    </div>\n  );\n}\n","type":"registry:ui"},{"path":"components/ui/domino-word.css","target":"components/ui/domino-word.css","content":"@keyframes domino-fall {\n  0%,\n  19%,\n  91%,\n  100% {\n    transform: rotateX(0) translateY(0);\n  }\n  40%,\n  66% {\n    transform: rotateX(79deg) translateY(13px);\n  }\n  72% {\n    transform: rotateX(-8deg) translateY(-5px);\n  }\n  80% {\n    transform: rotateX(3deg);\n  }\n}\n\n/*\n * The surface the tiles are designed to sit on. Also the positioning context\n * for the ground shadow, and where the 3D depth comes from.\n */\n.domino-word-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: radial-gradient(circle at 50% 45%, #ff9a7c, #ff5a40 50%, #bd3027);\n  perspective: 900px;\n}\n\n.domino-word {\n  /*\n   * A tile is 70px wherever there is room for it. When there is not — a narrow\n   * stage, or a long word — the whole rack narrows instead of overflowing, so the\n   * layout box keeps fitting and the grid above can still centre it. Everything\n   * below is a ratio of `--tile`, which is how the 70px design stays intact.\n   * `--n` is the letter count; the component passes it. 1.1143 is tile + gap.\n   */\n  --tile: min(70px, calc(96cqw / (var(--n, 6) * 1.1143)));\n  display: flex;\n  align-items: flex-end;\n  gap: calc(var(--tile) * 0.1143);\n  height: calc(var(--tile) * 2.1429);\n  perspective: 900px;\n}\n\n.domino-word > span {\n  --delay: calc(var(--i) * 0.13s);\n  position: relative;\n  width: var(--tile);\n  height: calc(var(--tile) * 1.5143);\n  display: grid;\n  place-items: center;\n  transform-style: preserve-3d;\n  transform-origin: 50% 100%;\n  animation: domino-fall 4.5s var(--delay) cubic-bezier(0.65, 0, 0.2, 1) infinite;\n}\n\n.domino-word span > b {\n  position: absolute;\n  inset: 0;\n  display: grid;\n  place-items: center;\n  border: 1px solid #111;\n  border-radius: 4px;\n  background: #f0ede6;\n  font-size: calc(var(--tile) * 0.8286);\n  line-height: 1;\n  box-shadow: 8px 10px 0 rgba(17, 17, 15, 0.13);\n}\n\n.domino-word span > i {\n  position: absolute;\n  left: calc(var(--tile) * 0.0429);\n  right: calc(var(--tile) * 0.0429);\n  bottom: calc(var(--tile) * -0.1571);\n  height: calc(var(--tile) * 0.1714);\n  background: #111;\n  transform: rotateX(90deg);\n  transform-origin: top;\n}\n\n.domino-word::before {\n  content: '';\n  position: absolute;\n  width: calc(var(--n, 6) * var(--tile) * 1.0714);\n  height: calc(var(--tile) * 1.3571);\n  border-radius: 50%;\n  background: rgba(83, 12, 8, 0.22);\n  filter: blur(20px);\n  transform: translateY(85px) rotateX(70deg);\n}\n\n.domino-word > span {\n  filter: drop-shadow(0 18px 18px rgba(86, 15, 10, 0.22));\n}\n\n.domino-word span > b {\n  background: linear-gradient(135deg, #fff, #e8e3d8);\n  box-shadow:\n    inset 0 0 0 5px rgba(255, 255, 255, 0.35),\n    8px 10px 0 rgba(17, 17, 15, 0.16);\n}\n\n.domino-word > span:nth-child(3) b {\n  background: #d8ff43;\n}\n\n.domino-word > span:nth-child(5) b {\n  background: #9b6dff;\n}\n\n/*\n * The card variant: the 298x240 catalogue frame, at that real size and never scaled.\n * There is no copy in this stage to drop — the rack is the whole composition — so the\n * work here is geometry. The container query already narrows a tile to 43px at 298px and\n * that is where it has to stay: six of them and five gaps are 281px of the 298. What did\n * not follow the tile down is everything drawn in absolute pixels for the 70px one. The\n * rack came out 65px tall in a 240px frame with 101px of empty gradient above it, and the\n * ground blob — an 85px translate under a 92px rack — landed 46px clear of the feet, a\n * detached smudge across the bottom edge.\n *\n * So the tile keeps its width and gains its height back, and the pixel offsets below are\n * re-tied to `--tile`. No `border-radius: 0` line, because this stage never set one and\n * the card frame rounds and clips already. No `touch-action` either: the rack takes no\n * pointer events at all — the fall is a CSS loop, running before the card is even\n * scrolled to — so a finger on the card scrolls the page, and `pan-y` would only cost it\n * the pinch-zoom.\n */\n.domino-word-stage[data-compact='true'] {\n  min-height: 0;\n  height: 100%;\n}\n\n/*\n * 106px, and a constant rather than a ratio of `--tile`: it is the height the shipped 70px\n * tile has, and what it has to stay in proportion with is constant too — the fall\n * keyframes' fixed 13px drop and 5px rebound, and the 900px perspective. As a multiple of\n * the tile it would be 106px only at the one card width it was worked out for, and the\n * grid does not hold still at that width: `ItemGrid` runs one to four columns inside\n * `container-page`, so a card is 286px across at 640px of viewport and 607px across at\n * 639px. The tile is capped at 70px, and 2.4762 of that is a 173px rack in a 240px frame —\n * feet at y=207 and the ground shadow back out of the bottom edge, which is the exact\n * failure the rest of this block exists to fix. Held at 106px the feet stay at y=173 at\n * every card width, and the widening tile only walks the face back toward the shipped\n * 1:1.5 proportion.\n *\n * At the 43px tile of a 298px card that face is nearer 1:2.5, so there is more blank face\n * around a letter that cannot grow with it: `M` much over 35px stops clearing a 43px face.\n * It gives the mechanism 106px of the 240 instead of 65.\n *\n * The rack box is set to that same 106px rather than the 1.4x of it that it carries at\n * full size. The extra is headroom above the tiles, and at this tile height it would put\n * the feet at y=195 of 240 and crush the ground shadow into the bottom edge. Without it\n * the tiles are exactly centred, feet at y=173, and the shadow has the strip below them.\n */\n.domino-word-stage[data-compact='true'] .domino-word {\n  height: 106px;\n}\n\n.domino-word-stage[data-compact='true'] .domino-word > span {\n  height: 106px;\n  /* 18px of offset and blur under a 43px tile with 5px of gap merges the six shadows into\n     one band, and the blob below is already doing that job; 11px keeps them per-tile. */\n  filter: drop-shadow(0 calc(var(--tile) * 0.2571) calc(var(--tile) * 0.2571) rgba(86, 15, 10, 0.22));\n}\n\n/*\n * Both layers restated, because `box-shadow` cannot be overridden a layer at a time. The\n * hard offset is the one that had to move: at full size it is 8px against an 8px gap, the\n * same 0.1143 of the tile, so the cast shadow reaches exactly to the next tile. Left at a\n * literal 8px against the card's 5px gap it crosses into the neighbour by 3px. The inset\n * bevel is 7% of a 70px face and 12% of a 43px one, which reads as a thick rim around a\n * small letter; the ratio puts it back to 3px.\n */\n.domino-word-stage[data-compact='true'] .domino-word span > b {\n  box-shadow:\n    inset 0 0 0 calc(var(--tile) * 0.0714) rgba(255, 255, 255, 0.35),\n    calc(var(--tile) * 0.1143) calc(var(--tile) * 0.1429) 0 rgba(17, 17, 15, 0.16);\n}\n\n/*\n * The ground shadow, the one thing in the file whose offset is large enough to leave the\n * frame. 85px was 1.2143 of the 70px tile; as that ratio it is 52px here, which lands the\n * blob's band at y=186..206 under feet at y=173 instead of 46px adrift of them, and keeps\n * 22px between it and the bottom edge. The blur goes with it: 20px on a blob shrunk from\n * 450x95 to 275x58 is a smear rather than a shadow, and 12px holds the same edge.\n */\n.domino-word-stage[data-compact='true'] .domino-word::before {\n  filter: blur(calc(var(--tile) * 0.2857));\n  transform: translateY(calc(var(--tile) * 1.2143)) rotateX(70deg);\n}\n\n/*\n * `domino-fall` rests upright — 0%, 19%, 91% and 100% are all `rotateX(0) translateY(0)` —\n * so switching the loop off leaves every tile standing and the word readable. That is the\n * right still for a domino: fallen tiles are the transient state, not the subject.\n */\n@media (prefers-reduced-motion: reduce) {\n  .domino-word > span {\n    animation: none;\n  }\n}\n","type":"registry:file"}],"meta":{"kind":"animations","categories":["text","three-d","stagger"],"docs":"https://ui.artbloom.tech/artbloom/animations/domino-word"}}