{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sheen-headline",
  "type": "registry:block",
  "title": "Sheen Headline",
  "description": "A large headline in a metallic fill (chrome, gold, pearl or ink) with a band of light that follows the pointer's position and angle on a spring, and drifts back to a slow idle sweep. Real text, painted per line with its own horizon.",
  "docs": "Sheen Headline was added to public/vividsites/sheen-headline.html. Paste its markup, <style> and <script> where you want it on the page (or open the file to see it alone). Customise it with the CSS variables at the top. Docs and live demo: https://vividsites.app/c/sheen-headline",
  "categories": [
    "ui",
    "text-type"
  ],
  "files": [
    {
      "path": "components/vividsites/sheen-headline.html",
      "type": "registry:file",
      "target": "public/vividsites/sheen-headline.html",
      "content": "<!--\n  vs-sheen-headline: a large headline in a metallic fill with a band of light moving across the\n  letters. The light follows the pointer (position AND angle, on a spring), and when the pointer\n  leaves it goes back to a slow idle sweep. Two layers of light: a tight specular line and a broad\n  soft glow, over a material ramp with a horizon line, so it reads as metal rather than a stripe.\n\n  Markup: <h1 class=\"vs-sheen-headline\" data-vs-sheen-headline=\"chrome\">Gilded hour</h1>\n    data-vs-sheen-headline = \"chrome\" | \"gold\" | \"pearl\" | \"ink\"   the material preset\n    data-vs-sheen-headline-follow = \"off\"   ignore the pointer, idle sweep only\n  Any face works; heavy or high-contrast display faces show the material best.\n  Text stays real text (selectable, readable by screen readers); only its fill is painted.\n\n  Variables:\n    --vs-sheen-headline-fill     the material: any background image (set by the preset)\n    --vs-sheen-headline-glint    colour of the light band                    (preset)\n    --vs-sheen-headline-width    half-width of the specular line, % of the text  7%\n    --vs-sheen-headline-glow     strength of the broad glow, 0–1               .42\n    --vs-sheen-headline-period   idle sweep period                            4.2s\n    --vs-sheen-headline-angle    base angle of the band                        104deg\n  JS writes --vs-sheen-headline-x (band position) and --vs-sheen-headline-a (angle); don't set those.\n  Paused off-screen. prefers-reduced-motion: the band rests at 32%, no sweep.\n-->\n<style>\n  .vs-sheen-headline {\n    --vs-sheen-headline-width: 7%;\n    --vs-sheen-headline-glow: .42;\n    --vs-sheen-headline-period: 4.2s;\n    --vs-sheen-headline-angle: 104deg;\n    --vs-sheen-headline-x: 32%;\n    --vs-sheen-headline-a: var(--vs-sheen-headline-angle);\n    --vs-sheen-headline-glint: 255 255 255;\n    --vs-sheen-headline-fill: linear-gradient(180deg, #f6f8fa 0%, #b9c1ca 36%, #414852 49%, #1f242b 51%, #cfd6de 60%, #8d96a1 100%);\n    background-image:\n      linear-gradient(var(--vs-sheen-headline-a),\n        transparent calc(var(--vs-sheen-headline-x) - var(--vs-sheen-headline-width)),\n        rgb(var(--vs-sheen-headline-glint) / .95) var(--vs-sheen-headline-x),\n        transparent calc(var(--vs-sheen-headline-x) + var(--vs-sheen-headline-width))),\n      linear-gradient(var(--vs-sheen-headline-a),\n        transparent calc(var(--vs-sheen-headline-x) - var(--vs-sheen-headline-width) * 5),\n        rgb(var(--vs-sheen-headline-glint) / var(--vs-sheen-headline-glow)) var(--vs-sheen-headline-x),\n        transparent calc(var(--vs-sheen-headline-x) + var(--vs-sheen-headline-width) * 5)),\n      var(--vs-sheen-headline-fill);\n    /* the material repeats per line, so every line gets its own horizon */\n    background-size: 100% 100%, 100% 100%, 100% 1lh;\n    background-repeat: no-repeat, no-repeat, repeat-y;\n    -webkit-background-clip: text; background-clip: text;\n    color: transparent; -webkit-text-fill-color: transparent;\n    /* a hairline of shadow under each letter gives the metal an edge */\n    filter: drop-shadow(0 .018em 0 rgb(0 0 0 / .35)) drop-shadow(0 .12em .18em rgb(0 0 0 / .18));\n    padding-bottom: .06em;   /* keeps descenders inside the painted box */\n    -webkit-box-decoration-break: clone; box-decoration-break: clone;\n  }\n  .vs-sheen-headline[data-vs-sheen-headline=\"gold\"] {\n    --vs-sheen-headline-glint: 255 246 214;\n    --vs-sheen-headline-fill: linear-gradient(180deg, #fff3cf 0%, #e8bd62 34%, #7a4c14 49%, #4a2c08 51%, #f2cf78 60%, #a8741f 100%);\n  }\n  .vs-sheen-headline[data-vs-sheen-headline=\"pearl\"] {\n    --vs-sheen-headline-glint: 255 250 255;\n    --vs-sheen-headline-fill:\n      linear-gradient(100deg, rgb(255 190 225 / .55), rgb(190 240 255 / .5) 35%, rgb(215 255 215 / .45) 65%, rgb(255 215 190 / .55)),\n      linear-gradient(180deg, #ffffff 0%, #e4e6f0 40%, #aab0c4 50%, #9097ad 52%, #eef0f7 62%, #c9cedd 100%);\n  }\n  .vs-sheen-headline[data-vs-sheen-headline=\"ink\"] {\n    --vs-sheen-headline-glint: 255 255 255;\n    --vs-sheen-headline-glow: .18;\n    --vs-sheen-headline-fill: linear-gradient(180deg, #2a2d34 0%, #16181d 48%, #050608 52%, #23262d 100%);\n    filter: none;\n  }\n  @media (prefers-reduced-motion: reduce) { .vs-sheen-headline { --vs-sheen-headline-x: 32% !important; } }\n</style>\n<script>\n(() => {\n  if (window.VsSheenHeadline) return;\n  const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;\n  const list = new Set();\n  let px = 0, py = 0, lastMove = -1e9;\n  addEventListener('pointermove', (e) => { px = e.clientX; py = e.clientY; lastMove = performance.now(); kick(); }, { passive: true });\n  document.addEventListener('pointerleave', () => { lastMove = -1e9; });\n\n  function mount(el) {\n    if (el.vsSheen) return;\n    const rec = { el, x: 32, v: 0, a: 0, av: 0, visible: true, phase: 0 };\n    el.vsSheen = rec; list.add(rec);\n    new IntersectionObserver(([e]) => { rec.visible = e.isIntersecting; kick(); }).observe(el);\n  }\n\n  let raf = 0, last = 0;\n  function frame(now) {\n    const dt = Math.min(.05, (now - (last || now)) / 1000); last = now;\n    let any = false;\n    list.forEach((r) => {\n      if (!r.visible) return; any = true;\n      const cs = getComputedStyle(r.el);\n      const period = Math.max(.6, parseFloat(cs.getPropertyValue('--vs-sheen-headline-period')) || 4.2) * (/ms$/.test(cs.getPropertyValue('--vs-sheen-headline-period').trim()) ? .001 : 1);\n      const base = parseFloat(cs.getPropertyValue('--vs-sheen-headline-angle')) || 104;\n      const b = r.el.getBoundingClientRect();\n      const follow = r.el.dataset.vsSheenHeadlineFollow !== 'off' && now - lastMove < 1600;\n      let gx, ga = 0;\n      if (follow) {\n        // the band sits where the pointer is (projected across the text), angle leans with its height\n        gx = ((px - b.left) / Math.max(1, b.width)) * 100;\n        gx = Math.max(-25, Math.min(125, gx));\n        ga = Math.max(-1, Math.min(1, (py - (b.top + b.height / 2)) / Math.max(200, b.height * 1.5))) * 24;\n        r.phase = (gx + 30) / 190;                       // idle resumes from here\n      } else {\n        // idle: sweep across, rest off the right edge, come round again\n        r.phase = (r.phase + dt / period) % 1;\n        const p = Math.min(1, r.phase / .62), e = p < .5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2;\n        gx = -30 + 190 * e;\n      }\n      if (gx < r.x - 100) { r.x = gx; r.v = 0; }         // wrap: jump back without sweeping backwards\n      const k = follow ? 170 : 400, c = follow ? 24 : 40;\n      for (let h = 0; h < dt; h += 1 / 120) {\n        const s = Math.min(1 / 120, dt - h);\n        r.v += (-k * (r.x - gx) - c * r.v) * s; r.x += r.v * s;\n        r.av += (-90 * (r.a - ga) - 18 * r.av) * s; r.a += r.av * s;\n      }\n      r.el.style.setProperty('--vs-sheen-headline-x', r.x.toFixed(2) + '%');\n      r.el.style.setProperty('--vs-sheen-headline-a', (base + r.a).toFixed(2) + 'deg');\n    });\n    raf = any ? requestAnimationFrame(frame) : 0;\n  }\n  function kick() { if (!raf && !reduced) { last = 0; raf = requestAnimationFrame(frame); } }\n\n  window.VsSheenHeadline = { mount: (el) => { mount(el); kick(); } };\n  const init = () => { document.querySelectorAll('[data-vs-sheen-headline]').forEach(mount); kick(); };\n  document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', init) : init();\n})();\n</script>\n\n<!-- usage -->\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link href=\"https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500&display=swap\" rel=\"stylesheet\">\n<div style=\"background:#0b0b10;padding:16vh 6vw;text-align:center\">\n  <h1 class=\"vs-sheen-headline\" data-vs-sheen-headline=\"chrome\"\n      style=\"margin:0;font:500 clamp(3.5rem,13vw,11rem)/.92 'Fraunces',serif;font-variation-settings:'opsz' 144;letter-spacing:-.035em\">\n    Gilded hour\n  </h1>\n</div>\n"
    }
  ],
  "meta": {
    "category": "ui",
    "group": "Text & type",
    "tags": [
      "shine",
      "shimmer",
      "metallic",
      "gold",
      "chrome",
      "headline",
      "text effect",
      "hover"
    ],
    "tech": [
      "CSS",
      "JS"
    ],
    "deps": [
      "https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500&display=swap"
    ],
    "source": null,
    "free": true,
    "accent": "#e8bd62",
    "added": "2026-09-30",
    "demo": "https://vividsites.app/api/component/sheen-headline/index.html",
    "thumbnail": "https://vividsites.app/api/component/sheen-headline/thumb.jpg",
    "video": "https://vividsites.app/api/component/sheen-headline/thumb.mp4",
    "page": "https://vividsites.app/c/sheen-headline"
  }
}