{"$schema":"https://ui.artbloom.tech/schema/registry-item.json","name":"kinetic-kerning","type":"registry:ui","title":"Kinetic Kerning","description":"Letters slide out of true and settle back into precise spacing, one after another, with the caption arriving last.","author":"@artbloom","dependencies":[],"registryDependencies":[],"files":[{"path":"components/ui/kinetic-kerning.tsx","target":"components/ui/kinetic-kerning.tsx","content":"'use client';\n\nimport type { CSSProperties } from 'react';\n\nimport './kinetic-kerning.css';\n\nexport type KineticKerningProps = {\n  /** One letter per glyph slot. */\n  word?: string;\n  /** The line that settles underneath. */\n  caption?: string;\n  /**\n   * The 298x240 catalogue card: the same word on the same keyframe, sized to the\n   * card rather than to a marketing section, with the tagline dropped.\n   * Presentation only — see `kinetic-kerning.css`. Nothing in here is focusable in\n   * either mode, since the word is a `role=\"img\"` and not a control, so the card's\n   * tab order needs no help from this component.\n   */\n  compact?: boolean;\n};\n\nexport function KineticKerning({\n  word = 'KERNING',\n  caption = 'SPACING IS MOTION',\n  compact = false,\n}: KineticKerningProps = {}) {\n  const letters = [...word];\n  return (\n    <div className=\"kinetic-kerning-stage\" data-compact={compact ? 'true' : undefined}>\n      <div\n        className=\"kinetic-kerning\"\n        role=\"img\"\n        aria-label={`${word} letters shifting into precise spacing`}\n        style={\n          {\n            // The stylesheet divides the stage width by `--n` to pick a size, and\n            // scatters each letter away from `--mid`, so any word stays centred.\n            '--n': letters.length,\n            '--mid': (letters.length - 1) / 2,\n          } as CSSProperties\n        }\n      >\n        {letters.map((letter, index) => (\n          <i style={{ '--i': index } as CSSProperties} key={`${letter}-${index}`}>\n            {letter}\n          </i>\n        ))}\n        <span>{caption}</span>\n      </div>\n    </div>\n  );\n}\n","type":"registry:ui"},{"path":"components/ui/kinetic-kerning.css","target":"components/ui/kinetic-kerning.css","content":"@keyframes kern-lock {\n  0%,\n  14%,\n  90%,\n  100% {\n    transform: translateX(calc((var(--i) - var(--mid)) * 0.34em))\n      rotate(calc((var(--i) - var(--mid)) * 8deg)) scale(0.72);\n    filter: blur(7px);\n    opacity: 0.18;\n  }\n  38%,\n  66% {\n    transform: translateX(0) rotate(0) scale(1);\n    filter: blur(0);\n    opacity: 1;\n  }\n  48% {\n    transform: translateX(calc((var(--i) - var(--mid)) * -0.023em)) scale(1.04);\n  }\n}\n\n/* The surface the word is designed to sit on. Size it from the parent. */\n.kinetic-kerning-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: #111;\n  color: #f0ede6;\n}\n\n.kinetic-kerning {\n  position: relative;\n  width: 100%;\n  height: 100%;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  overflow: hidden;\n}\n\n.kinetic-kerning::before {\n  content: '';\n  position: absolute;\n  left: 8%;\n  right: 8%;\n  top: 50%;\n  height: 1px;\n  background: rgba(216, 255, 67, 0.45);\n}\n\n.kinetic-kerning > i {\n  position: relative;\n  /* Stage-relative, divided by the letter count: the word fits at any width and\n     at any length, and the scatter in `kern-lock` is in `em` so it tracks it. */\n  font-size: clamp(20px, calc(135cqw / var(--n, 7)), 132px);\n  font-weight: 800;\n  font-style: normal;\n  line-height: 0.8;\n  letter-spacing: -0.12em;\n  color: #f0ede6;\n  text-shadow: 0 18px 30px rgba(0, 0, 0, 0.55);\n  animation: kern-lock 5.8s calc(var(--i) * 70ms) cubic-bezier(0.76, 0, 0.24, 1) infinite;\n}\n\n.kinetic-kerning > i:nth-of-type(2) {\n  color: #d8ff43;\n}\n\n.kinetic-kerning > i:nth-of-type(4) {\n  color: #ff5a40;\n}\n\n.kinetic-kerning > span {\n  position: absolute;\n  left: 16px;\n  bottom: 15px;\n  color: #9a9a92;\n  font: 8px ui-monospace, SFMono-Regular, Menlo, monospace;\n  letter-spacing: 0.13em;\n}\n\n/*\n * The card variant: the 298x240 catalogue frame, at that real size and never scaled.\n *\n * There is almost no copy to cut here — the word *is* the composition — so the job is\n * scale. The stage was authored against a 320px section, and dropped into a card the\n * same clamp hands it 57px letters: a word across roughly seventy per cent of the\n * width sitting in a 46px line box inside a 240px frame, so most of the card is empty\n * ground with a hairline through it. Everything below is measured against the frame\n * rather than the viewport, and can be: the stage declares `container-type:\n * inline-size`, so every `cqw` in this file already reads 298px, and there is not a\n * viewport unit anywhere in it.\n *\n * `min-height` is the only sizing the stage asserts, and there is no radius to drop —\n * the card frame rounds and clips. No `touch-action` either, and none needed: the\n * component has no pointer handler and no drag surface, so a swipe over the card\n * scrolls the page as it should.\n */\n.kinetic-kerning-stage[data-compact='true'] {\n  min-height: 0;\n  height: 100%;\n}\n\n/*\n * Edge to edge. The 8% inset leaves a 250px rail at this width, which stops within a\n * few pixels of the word's own ends and so reads as an underline that happens to fit —\n * the opposite of the full-size composition, where the rail runs a long way past the\n * letters and is clearly a guide they are being aligned to. Full bleed puts both ends\n * back outside the word.\n */\n.kinetic-kerning-stage[data-compact='true'] .kinetic-kerning::before {\n  left: 0;\n  right: 0;\n}\n\n/*\n * 160cqw over the letter count instead of 135, which is a flat eighteen per cent more\n * word across the same 298px however the face measures: 68px letters, a 54px line box,\n * and about twenty-five pixels of ground either side of the settled word. That margin\n * is the point of stopping at 160 — 180cqw would fill the width outright and leaves\n * nothing for a fallback face wider than Inter to spend. The two bounds are carried\n * over untouched; at seven letters neither one is reached in a card.\n *\n * The scatter is written in `em`, so it grows with the size and keeps its proportions,\n * and it cannot be made to fit. At peak the outermost letter is thrown 1.02em clear of\n * its slot on top of the half-word it already stands from the centre, which is outside\n * a 298px frame at any size where the settled word is still worth reading. It leaves\n * whole rather than being sliced down the middle, at 0.18 opacity under a 7px blur, and\n * what the card shows is letters arriving from beyond its own edges.\n */\n.kinetic-kerning-stage[data-compact='true'] .kinetic-kerning > i {\n  font-size: clamp(20px, calc(160cqw / var(--n, 7)), 132px);\n  /*\n   * Started 38% in — the first frame of the locked plateau — so a card mounting on\n   * scroll opens on the word snapping true instead of on three quarters of a second of\n   * blurred ghosts. The 70ms stagger is unchanged and still runs forward, so the first\n   * letter is home while the last is closing the final third of its approach.\n   */\n  animation-delay: calc(var(--i) * 70ms - 2.2s);\n}\n\n/*\n * The tagline goes. It is a second line of copy, and a static one — nothing animates it\n * — set at 8px, which at card width is a smudge in a corner rather than a label, and\n * the card's own title and description already carry the words. Nothing is kept in its\n * place: this loop needs no instruction, because it is running before the visitor has\n * done anything to it.\n */\n.kinetic-kerning-stage[data-compact='true'] .kinetic-kerning > span {\n  display: none;\n}\n\n/*\n * The loop spends its ends scattered and blurred at opacity 0.18 and its middle locked\n * into place, so the still has to be the middle — the 38%–66% pose, where every letter is\n * at translateX(0), unblurred and fully opaque. Reduced motion gets the kerned wordmark,\n * which is the thing the animation exists to arrive at.\n */\n@media (prefers-reduced-motion: reduce) {\n  .kinetic-kerning > i {\n    animation: none;\n    transform: none;\n    filter: none;\n    opacity: 1;\n  }\n}\n","type":"registry:file"}],"meta":{"kind":"animations","categories":["text","stagger","springs"],"docs":"https://ui.artbloom.tech/artbloom/animations/kinetic-kerning"}}