{"$schema":"https://ui.artbloom.tech/schema/registry-item.json","name":"aurora-thread-loom","type":"registry:ui","title":"Aurora Thread Loom","description":"Forty-eight gradient threads waving out of phase and hue-rotating as they cross. One keyframe, indexed per thread by a custom property.","author":"@artbloom","dependencies":[],"registryDependencies":[],"files":[{"path":"components/ui/aurora-thread-loom.tsx","target":"components/ui/aurora-thread-loom.tsx","content":"'use client';\n\nimport type { CSSProperties } from 'react';\n\nimport './aurora-thread-loom.css';\n\n/** 48 threads: the stylesheet's wave math is centred on thread 24. */\nconst THREADS = 48;\n\n/**\n * `compact` is the 298x240 catalogue card: the same forty-eight threads on the same\n * keyframe, handed the whole frame, with the spec caption dropped and the wordmark\n * off its hero scale. Presentation only, and all of it in `aurora-thread-loom.css`.\n *\n * Nothing else has to change for a card. The loom runs on its own clock, so the tile\n * is alive on load with no interaction to invite; it takes no pointer input, so it\n * never claims the vertical gesture; and it renders no control, so there is nothing\n * focusable for the card's `aria-hidden` frame to trap.\n */\nexport type AuroraThreadLoomProps = { compact?: boolean };\n\nexport function AuroraThreadLoom({ compact = false }: AuroraThreadLoomProps) {\n  return (\n    <div\n      className=\"aurora-thread-loom-stage\"\n      data-compact={compact ? 'true' : undefined}\n    >\n      <div\n        className=\"aurora-thread-loom\"\n        role=\"img\"\n        aria-label=\"Luminous threads weaving an aurora textile\"\n      >\n        <div>\n          {Array.from({ length: THREADS }, (_, index) => (\n            <i style={{ '--i': index } as CSSProperties} key={index} />\n          ))}\n        </div>\n        <b>AURORA</b>\n        <span>LIGHT LOOM / 048 THREADS</span>\n      </div>\n    </div>\n  );\n}\n","type":"registry:ui"},{"path":"components/ui/aurora-thread-loom.css","target":"components/ui/aurora-thread-loom.css","content":"@keyframes loom-wave {\n  0%,\n  100% {\n    transform: translateY(calc((var(--i) - 24) * -0.9px)) scaleY(0.22) rotate(0);\n    opacity: 0.35;\n    filter: hue-rotate(0);\n  }\n  50% {\n    transform: translateY(calc((var(--i) - 24) * 1.8px)) scaleY(1)\n      rotate(calc((var(--i) - 24) * 0.35deg));\n    opacity: 1;\n    filter: hue-rotate(calc(var(--i) * 5deg));\n  }\n}\n\n/* The surface the loom is designed to sit on. Size it from the parent. */\n.aurora-thread-loom-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: #030913;\n  color: white;\n}\n\n.aurora-thread-loom {\n  position: relative;\n  width: 100%;\n  height: 100%;\n  display: grid;\n  place-items: center;\n  overflow: hidden;\n}\n\n.aurora-thread-loom::before {\n  content: '';\n  position: absolute;\n  inset: 0;\n  background:\n    radial-gradient(ellipse at 50% 55%, rgba(49, 85, 231, 0.32), transparent 58%),\n    linear-gradient(\n      90deg,\n      transparent 49.8%,\n      rgba(255, 255, 255, 0.12) 50%,\n      transparent 50.2%\n    );\n}\n\n.aurora-thread-loom > div {\n  position: relative;\n  /* 540px is the design width; below a ~590px stage the loom narrows with it\n     rather than overflowing, since 49 threads cannot be squeezed by `flex` alone. */\n  width: min(540px, 92cqw);\n  height: 330px;\n  display: flex;\n  align-items: center;\n  /* At the design width this clamps to the 5px it was drawn with. */\n  gap: clamp(1.5px, 0.95cqw, 5px);\n  filter: drop-shadow(0 0 18px rgba(85, 255, 204, 0.25));\n}\n\n.aurora-thread-loom div > i {\n  flex: 1;\n  height: 100%;\n  /* Only reached on a very narrow stage; `flex` keeps them ~6px when there is room. */\n  min-width: 1px;\n  border-radius: 50%;\n  background: linear-gradient(\n    180deg,\n    transparent,\n    #45f0c0 28%,\n    #3155e7 52%,\n    #b865ff 73%,\n    transparent\n  );\n  transform-origin: center;\n  animation: loom-wave 5.4s calc(var(--i) * -75ms) cubic-bezier(0.45, 0, 0.55, 1) infinite;\n}\n\n.aurora-thread-loom div > i:nth-child(even) {\n  background: linear-gradient(\n    180deg,\n    transparent,\n    #d8ff43 24%,\n    #35c9ff 55%,\n    #ff5a9e 78%,\n    transparent\n  );\n  animation-direction: reverse;\n}\n\n.aurora-thread-loom > b {\n  position: absolute;\n  z-index: 3;\n  font-size: clamp(22px, 22cqw, 118px);\n  letter-spacing: -0.08em;\n  mix-blend-mode: difference;\n}\n\n.aurora-thread-loom > span {\n  position: absolute;\n  left: 18px;\n  bottom: 16px;\n  color: #79908e;\n  font: 8px ui-monospace, SFMono-Regular, Menlo, monospace;\n  letter-spacing: 0.13em;\n}\n\n/*\n * Card variant: the catalogue frame, at real pixels and never scaled. 298x240 in the\n * main grids — 240px is the floor every card surface uses, on a profile, in bookmarks\n * and in the related strip as well, and the landing lists ask for 260px — so nothing\n * below is allowed to assume a height. Widths run about 285-360px across the\n * breakpoints.\n *\n * (These notes used to describe a 200px frame on the profile, bookmarks and related\n * surfaces. There is no 200px frame: `ItemCard` and `ItemGrid` both default `height`\n * to 240 and every one of those surfaces goes through the grid. The arithmetic below\n * is redone against 240, which is the smallest frame that exists.)\n *\n * Most of this loom already answers to its container — the sheet's width, the gap\n * between threads and the wordmark are all `cqw`, and the wash and the centre seam are\n * percentages — so the compact block is short on purpose. What breaks at this size is\n * the geometry drawn for the 320px stage: a 330px thread row, and a wordmark scaled to\n * lead a hero. Both come down. The 8px spec strip goes entirely; the keyframes and the\n * comb are untouched, and so is the 18px bloom — relative to the thread pitch it\n * reaches about twice as far here as at design width (18px over a 5.7px pitch against\n * 11.3px), which turns the comb hazier and more sheet-like. For a tile of an aurora\n * that is the right trade, but it is the one call here worth checking in a browser.\n */\n\n/*\n * The 240px floor goes first, and it is not tidying: a stage held open to its own\n * 240px `min-height` inside an `overflow: hidden` box that is exactly 240px tall has\n * no room for the frame's own padding, and any surface that ever asks for less than\n * 240 loses the difference off both ends of the loom instead of fitting it. The height\n * comes from the frame, as it already did. Nothing to unround — the stage sets no\n * radius of its own and the card frame clips its own corners.\n */\n.aurora-thread-loom-stage[data-compact='true'] {\n  min-height: 0;\n  height: 100%;\n}\n\n/*\n * No `touch-action` line, and none wanted: the loom has no pointer handler, no hover\n * branch and no drag surface, so a full-bleed tile of it never takes the vertical\n * gesture and a touch on the card scrolls the page. Declaring `pan-y` here would only\n * cost the card its pinch-zoom.\n */\n\n/*\n * 80% of the frame — 192px in a 240px card — down from the 330px the row was drawn at.\n * The row height is only half of what decides the fit: each thread is also translated\n * by `(var(--i) - 24) * 1.8px` at the peak of its own cycle, 43px for thread 0 and 41px\n * for thread 47, so the threads at the ends of the comb cross the clip whatever the row\n * does. What the fraction buys is how much of one gets taken.\n *\n * At 192px centred in 240px, thread 0 loses 19px, a tenth of its own length. On the\n * shipped 320px stage the same thread loses 48px of 330px — 15% — and the row there\n * overhangs the frame by 5px at each end on its own, so at the peak of its cycle every\n * thread is cut; here it is threads 0-10 and 38-47. So the card takes less off the swell\n * than the composition it was approved at, not more. 90% is where the two fractions\n * meet; 80% leaves 24px of air above and below the row at rest, where the keyframe holds\n * the sheet at `scaleY(0.22)`.\n *\n * And what the clip takes is the tapered end of a thread rather than its body:\n * `border-radius: 50%` makes each `i` an ellipse, ~60% of full width at the height of\n * the cut, and the gradient is still climbing out of `transparent` there — the first\n * colour stop sits at 28%, so a cut at 10% lands near 0.4 alpha. The `-75ms` per-thread\n * stagger spreads the peaks along the comb, so this is a travelling event at the two\n * ends and never a straight cut across the sheet.\n *\n * Left at 330px, by contrast, thread 0 would be cut 88px into a 240px frame — 27% of\n * its length, which is the far edge of that 28% ramp and effectively solid colour.\n *\n * A percentage and not `cqh`: the stage is an `inline-size` container, so `cqh` has no\n * block-axis container to resolve against here. A fraction also follows the frame,\n * which is the thing that varies — 240px in every grid, 260px in the landing lists. At\n * the 240px end the cut is the 10% worked through above, which is the worst it gets.\n */\n.aurora-thread-loom-stage[data-compact='true'] .aurora-thread-loom > div {\n  height: 80%;\n}\n\n/*\n * ~42px in a 298px card. The clamp already measures the right container — `22cqw` is\n * 66px here, not the 118px it reaches on a hero — so nothing overflows; what is wrong is\n * the share of the frame it takes. Six caps at -0.08em run roughly 3.7x the font size in\n * the inherited sans, so 22cqw sets a word about 240px wide inside 298px and the mark\n * stops sitting on the sheet and becomes a lid over it. On any stage wide enough to reach\n * the 118px ceiling the same word takes a third to a half of the width and the threads\n * are what you see first; 14cqw puts that ratio back — about 155px of 298 — and stays a\n * fraction of the container, so it tracks the real card widths rather than the one\n * nominal 298.\n */\n.aurora-thread-loom-stage[data-compact='true'] .aurora-thread-loom > b {\n  font-size: 14cqw;\n}\n\n/*\n * The spec strip goes. \"LIGHT LOOM / 048 THREADS\" restates the card's own title and\n * description a few pixels above them, and the one line a card may keep is meant for\n * an instruction — for something the visitor has to hover or drag to see move. This\n * loom asks nothing of anyone, so there is nothing to instruct, and 8px of mono at the\n * bottom-left of a tile this size is debris.\n */\n.aurora-thread-loom-stage[data-compact='true'] .aurora-thread-loom > span {\n  display: none;\n}\n\n/*\n * The loop's own resting pose (0%/100%) is a dim flat bundle at scaleY(0.22) — freezing\n * there would read as a broken component rather than a still one. So this pins every\n * thread to the 50% pose instead: full height, full opacity, each one's own rotation and\n * hue offset intact. The wave stops moving and stays woven.\n */\n@media (prefers-reduced-motion: reduce) {\n  .aurora-thread-loom div > i {\n    animation: none;\n    transform: translateY(calc((var(--i) - 24) * 1.8px)) scaleY(1)\n      rotate(calc((var(--i) - 24) * 0.35deg));\n    opacity: 1;\n    filter: hue-rotate(calc(var(--i) * 5deg));\n  }\n}\n","type":"registry:file"}],"meta":{"kind":"animations","categories":["gradients","backgrounds","infinite"],"docs":"https://ui.artbloom.tech/artbloom/animations/aurora-thread-loom"}}