{"$schema":"https://ui.artbloom.tech/schema/registry-item.json","name":"split-shutter","type":"registry:ui","title":"Split Shutter","description":"Nine shutters part to reveal a word, each slat carrying its own printed label so the mechanism stays visible.","author":"@artbloom","dependencies":[],"registryDependencies":[],"files":[{"path":"components/ui/split-shutter.tsx","target":"components/ui/split-shutter.tsx","content":"'use client';\n\nimport type { CSSProperties } from 'react';\n\nimport './split-shutter.css';\n\nexport type SplitShutterProps = {\n  /** The word the shutters part to reveal. */\n  word?: string;\n  /** Alternating text printed on the slats. */\n  slats?: readonly [string, string];\n  /** Corner marker. */\n  label?: string;\n  /**\n   * The 298x240 catalogue card: the same nine slats on the same loop, handed the\n   * whole frame at that real size and never scaled. Presentation only — the word,\n   * the slats and their timing are untouched. See `split-shutter.css`.\n   */\n  compact?: boolean;\n};\n\n/** Nine slats, not a prop: the stylesheet times them by `nth-child`. */\nconst SLATS = 9;\n\nexport function SplitShutter({\n  word = 'OPEN',\n  slats = ['SHUTTER', 'MOTION'],\n  label = 'APERTURE / 09',\n  compact = false,\n}: SplitShutterProps = {}) {\n  return (\n    <div className=\"split-shutter-stage\" data-compact={compact ? 'true' : undefined}>\n      <div\n        className=\"split-shutter\"\n        role=\"img\"\n        aria-label={`Shutters revealing the word ${word}`}\n        // The stylesheet divides the stage width by `--n` to size the word.\n        style={{ '--n': word.length } as CSSProperties}\n      >\n        <b>{word}</b>\n        <div>\n          {Array.from({ length: SLATS }, (_, index) => (\n            <i style={{ '--i': index } as CSSProperties} key={index}>\n              <span>{slats[index % 2]}</span>\n            </i>\n          ))}\n        </div>\n        <em>{label}</em>\n      </div>\n    </div>\n  );\n}\n","type":"registry:ui"},{"path":"components/ui/split-shutter.css","target":"components/ui/split-shutter.css","content":"@keyframes shutter-split {\n  0%,\n  18%,\n  90%,\n  100% {\n    transform: scaleY(1);\n  }\n  43%,\n  68% {\n    transform: scaleY(0);\n  }\n}\n\n@keyframes open-focus {\n  0%,\n  24%,\n  86%,\n  100% {\n    letter-spacing: -0.08em;\n    scale: 0.75;\n    filter: blur(8px);\n    opacity: 0.1;\n  }\n  44%,\n  67% {\n    letter-spacing: 0.02em;\n    scale: 1;\n    filter: blur(0);\n    opacity: 1;\n  }\n}\n\n/* The surface the shutters are designed to sit on. Size it from the parent. */\n.split-shutter-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: #f0ede6;\n}\n\n.split-shutter {\n  position: relative;\n  width: 100%;\n  height: 100%;\n  display: grid;\n  place-items: center;\n  overflow: hidden;\n  background: #d8ff43;\n}\n\n.split-shutter > b {\n  /* Stage-relative and divided by the letter count, so a long word still fits. */\n  font-size: clamp(20px, calc(135cqw / var(--n, 4)), 132px);\n  letter-spacing: -0.08em;\n}\n\n.split-shutter > div {\n  position: absolute;\n  inset: 0;\n  display: flex;\n}\n\n.split-shutter i {\n  position: relative;\n  width: 11.111%;\n  height: 100%;\n  overflow: hidden;\n  border-right: 1px solid rgba(240, 237, 230, 0.35);\n  background: #111;\n  transform-origin: top;\n  animation: shutter-split 5s calc(var(--i) * 0.055s) cubic-bezier(0.76, 0, 0.24, 1)\n    infinite;\n}\n\n.split-shutter i:nth-child(even) {\n  transform-origin: bottom;\n  background: #3155e7;\n}\n\n.split-shutter i > span {\n  position: absolute;\n  left: 50%;\n  top: 50%;\n  color: #f0ede6;\n  font: 700 10px ui-monospace, SFMono-Regular, Menlo, monospace;\n  letter-spacing: 0.2em;\n  writing-mode: vertical-rl;\n  transform: translate(-50%, -50%);\n}\n\n.split-shutter > em {\n  position: absolute;\n  z-index: 4;\n  left: 16px;\n  bottom: 15px;\n  font: 8px ui-monospace, SFMono-Regular, Menlo, monospace;\n  font-style: normal;\n  letter-spacing: 0.1em;\n  mix-blend-mode: difference;\n  color: white;\n}\n\n.split-shutter {\n  background: radial-gradient(circle, #efff9c, #d8ff43 60%, #a6bd32);\n}\n\n.split-shutter > b {\n  text-shadow: 0 18px 30px rgba(17, 17, 15, 0.2);\n  animation: open-focus 5s ease-in-out infinite;\n}\n\n.split-shutter i {\n  box-shadow: 8px 0 18px rgba(0, 0, 0, 0.18);\n  filter: saturate(1.2);\n}\n\n.split-shutter i:nth-child(3n) {\n  background: #ff5a40;\n}\n\n/*\n * The card variant: the 298x240 catalogue frame, at that real size and never scaled.\n *\n * There is no prose here to drop. The composition is nine slats over one word, the slats\n * are already `inset: 0` and the stage carries no padding, so the mechanism has the whole\n * frame the moment the stage stops asking for 240px of its own height. What did not come\n * down with the container query is everything else — the word's drop shadow, the slats' cast\n * shadow and the corner marker's 8px, all chosen in absolute pixels against a stage several\n * times this wide.\n *\n * The word is the defect. `135cqw / 4` is 100px at a 298px container, and `open-focus`\n * tracks it out to +0.02em at the top of the reveal: four bold caps at 100px with tracking\n * on come to within a pixel or two of 298 across, so both ends sit on the frame edge and\n * `overflow: hidden` is what decides where the O starts. This is arithmetic off the advance\n * widths rather than a measured box — see the `> b` rule below for the number it was moved\n * to, and it stays a `cqw` clamp so a longer `word` still divides itself down. Its colour is\n * a second defect on the same element, and that one is not about this frame at all; the\n * `> b` rule carries both.\n *\n * No `border-radius: 0` line, because this stage never set one and the card frame rounds\n * and clips already. No `touch-action` either, and no `tabIndex` in the component: the\n * whole thing is a CSS loop over nine `<i>` elements with no pointer handler and nothing\n * focusable inside it, so a finger on the card scrolls the page as it should and `pan-y`\n * would only cost the card its pinch-zoom. Nothing has to be hovered or pressed for this\n * to move, which is why no instruction line was kept: the first slats part 0.9s after the\n * card mounts, on their own.\n */\n.split-shutter-stage[data-compact='true'] {\n  min-height: 0;\n  height: 100%;\n}\n\n/*\n * 108cqw is 80px at a 298px container: the word comes to roughly four fifths of the frame\n * at its widest keyframe, with some 30px of lime past each end, and its line box takes 97px\n * of the 240 instead of 120. The 20px floor and the 132px cap are the shipped ones and\n * neither binds here — the middle term governs for any word of three letters or more, and a\n * one or two letter word capped at 132px still clears both axes of this frame.\n *\n * 18px of drop under the 132px word the cap describes is a lift; under an 80px one it is a\n * second grey word below the first. The same 0.14 and 0.23 of the size are 11px and 18px.\n *\n * The ink is pinned to the slats' own #111 instead of being left to inherit. Nothing in this\n * file sets a colour on the word, so it takes the site's `--foreground` — and the site's\n * default theme is dark, which puts near-white type on a slab that is this same lime in both\n * themes: about 1.2:1, a ghost of a word in the one frame whose entire mechanism exists to\n * reveal it. #111 is the colour the composition was drawn for; the shipped drop shadow is\n * already `rgba(17, 17, 15, 0.2)`. Pinned here and not on `.split-shutter > b`, which would\n * be an edit to the shipped stage rather than an addition for the card.\n */\n.split-shutter-stage[data-compact='true'] .split-shutter > b {\n  font-size: clamp(20px, calc(108cqw / var(--n, 4)), 132px);\n  text-shadow: 0 11px 18px rgba(17, 17, 15, 0.2);\n  color: #111;\n}\n\n/*\n * Nine slats divide 298px into 33px each. An 8px offset with half of an 18px blur showing\n * past it throws the shadow 17px beyond a slat's own right edge — half the width of its\n * neighbour, nine times over, which is a grey wash where the subject is nine separately lit\n * edges parting in sequence. Held to the same fraction of the narrower slat it is 3px and\n * 6px: the seam still reads as depth and it stops before the next label.\n *\n * The labels themselves are left alone. They are 10px absolute at any stage width, and a\n * 33px slat holds a 10px vertical column with room either side; `SHUTTER` runs 56px of the\n * slat's 240 with the tracking, so it stays the printed mark it is meant to be.\n */\n.split-shutter-stage[data-compact='true'] .split-shutter i {\n  box-shadow: 3px 0 6px rgba(0, 0, 0, 0.18);\n}\n\n/*\n * The one piece of text kept, and it is a marker rather than a caption: it never meets the\n * word — 90px along the bottom edge, with the word's line box stopping some 40px above it —\n * and the difference blend goes on taking its colour off whichever slat is beneath it, so it\n * is legible against black, blue, red and lime in turn. Legible except for the size: 8px in a\n * frame this small is a smudge rather than a mark, and 10px is the floor for mono. Its 16px\n * and 15px insets are left as they are: read against 298px rather than a full-width stage\n * they are already a corner inset rather than a hairline.\n */\n.split-shutter-stage[data-compact='true'] .split-shutter > em {\n  font-size: 10px;\n}\n\n/*\n * The two loops are locked to each other: slats closed while the headline is blurred out,\n * slats open while it is legible. Freezing at the closed end would leave a card with no\n * readable content on it, so this pins the open end — slats retracted, headline at its\n * 44%–67% pose. Reduced motion sees the reveal's result, which is the point of a reveal.\n */\n@media (prefers-reduced-motion: reduce) {\n  .split-shutter i {\n    animation: none;\n    transform: scaleY(0);\n  }\n\n  .split-shutter > b {\n    animation: none;\n    letter-spacing: 0.02em;\n    scale: 1;\n    filter: blur(0);\n    opacity: 1;\n  }\n}\n","type":"registry:file"}],"meta":{"kind":"animations","categories":["masks","text","transitions"],"docs":"https://ui.artbloom.tech/artbloom/animations/split-shutter"}}