{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "article-headings",
  "title": "Article Headings",
  "description": "Scramble-decoding editorial headings with accessible fallback text.",
  "files": [
    {
      "path": "src/components/effects/article-headings.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\n/*\n * SPDX-License-Identifier: MIT\n * Adapted from ThreeUI's Article Headings by Meng To.\n * Upstream: https://github.com/MengTo/threeui\n * Sources: src/shaders/article-headings/ArticleHeadings.tsx and\n * src/shaders/article-headings/articleHeadingDecode.ts\n * Audited revision: fbc9b3d61b0ef4b2e93b42e4fffa617ca277429b\n */\n\nconst THREE_UI_NOTICE =\n  \"ThreeUI © 2026 Meng To · MIT · github.com/MengTo/threeui · fbc9b3d61b0ef4b2e93b42e4fffa617ca277429b\"\n\nexport type ArticleHeadingEntry = {\n  index?: string\n  title: string\n  meta?: string\n}\n\nexport type ArticleHeadingsProps = {\n  entries?: readonly ArticleHeadingEntry[]\n  mode?: \"dark\" | \"light\"\n  duration?: number\n  stagger?: number\n  scrambleLength?: number\n  className?: string\n  style?: React.CSSProperties\n}\n\nconst DEFAULT_ENTRIES: readonly ArticleHeadingEntry[] = [\n  {\n    index: \"01\",\n    title: \"Context Is Infrastructure, Not a Prompt\",\n    meta: \"SYSTEMS NOTE · 8 MIN\",\n  },\n  {\n    index: \"02\",\n    title: \"The Last Mile Is Verification\",\n    meta: \"FIELD GUIDE · 11 MIN\",\n  },\n  {\n    index: \"03\",\n    title: \"Interfaces Should Explain Their Own State\",\n    meta: \"DESIGN LOG · 6 MIN\",\n  },\n] as const\n\nconst DECODE_POOL = \"#%&@$/\\\\<>*+=~ABCDEFGHKMNPRSTUVWXYZ0123456789\"\n\nfunction clamp(value: number, min: number, max: number) {\n  return Math.max(min, Math.min(max, value))\n}\n\nfunction easedProgress(value: number) {\n  return 1 - Math.pow(1 - value, 3)\n}\n\nfunction scrambleText(\n  original: string,\n  revealed: number,\n  frame: number,\n  scrambleLength: number\n) {\n  let output = \"\"\n  for (let index = 0; index < original.length; index += 1) {\n    const character = original[index]\n    if (index < revealed || character === \" \") {\n      output += character\n      continue\n    }\n    const distance = index - revealed\n    if (distance > Math.max(0, scrambleLength)) {\n      output += character\n      continue\n    }\n    const poolIndex =\n      (frame * 7 + index * 13 + original.length * 3) % DECODE_POOL.length\n    output += DECODE_POOL[poolIndex]\n  }\n  return output\n}\n\nexport function ArticleHeadings({\n  entries = DEFAULT_ENTRIES,\n  mode = \"dark\",\n  duration = 560,\n  stagger = 140,\n  scrambleLength = 10,\n  className,\n  style,\n}: ArticleHeadingsProps) {\n  const rootRef = React.useRef<HTMLDivElement>(null)\n\n  React.useEffect(() => {\n    const root = rootRef.current\n    if (!root) return\n\n    const targets = Array.from(\n      root.querySelectorAll<HTMLElement>(\"[data-article-heading-text]\")\n    ).map((element) => ({\n      element,\n      original: element.getAttribute(\"data-value\") ?? element.textContent,\n    }))\n    const media = window.matchMedia(\"(prefers-reduced-motion: reduce)\")\n    let frameId = 0\n    let startedAt = performance.now()\n    let hiddenAt: number | null = null\n    let frameCount = 0\n    let disposed = false\n\n    const restore = () => {\n      targets.forEach(({ element, original }) => {\n        element.textContent = original\n      })\n    }\n\n    const draw = (now: number) => {\n      if (disposed || document.hidden || media.matches) return\n      frameCount += 1\n      const incomplete = targets\n        .map(({ element, original }, index) => {\n          const localTime = now - startedAt - index * Math.max(0, stagger)\n          const progress = clamp(localTime / Math.max(1, duration), 0, 1)\n          const revealed = Math.floor(easedProgress(progress) * original.length)\n          element.textContent = scrambleText(\n            original,\n            Math.max(0, Math.min(original.length, revealed)),\n            frameCount,\n            scrambleLength\n          )\n          return progress < 1\n        })\n        .some(Boolean)\n      if (incomplete) frameId = requestAnimationFrame(draw)\n    }\n\n    const start = (restart = false) => {\n      cancelAnimationFrame(frameId)\n      if (media.matches) {\n        restore()\n        return\n      }\n      if (restart) {\n        startedAt = performance.now()\n        frameCount = 0\n      }\n      frameId = requestAnimationFrame(draw)\n    }\n\n    const handleVisibility = () => {\n      if (document.hidden) {\n        hiddenAt = performance.now()\n        cancelAnimationFrame(frameId)\n        return\n      }\n      if (hiddenAt !== null) startedAt += performance.now() - hiddenAt\n      hiddenAt = null\n      start()\n    }\n    const handleMotion = () => start(true)\n\n    document.addEventListener(\"visibilitychange\", handleVisibility)\n    media.addEventListener(\"change\", handleMotion)\n    start(true)\n\n    return () => {\n      disposed = true\n      cancelAnimationFrame(frameId)\n      document.removeEventListener(\"visibilitychange\", handleVisibility)\n      media.removeEventListener(\"change\", handleMotion)\n      restore()\n    }\n  }, [duration, entries, scrambleLength, stagger])\n\n  return (\n    <section\n      ref={rootRef}\n      data-article-headings=\"\"\n      data-source-license={THREE_UI_NOTICE}\n      data-mode={mode}\n      className={className}\n      style={style}\n      aria-label=\"Article index\"\n    >\n      <style>{`\n        [data-article-headings] .article-heading__rule {\n          transform: scaleX(.22);\n          transform-origin: left;\n          transition: transform 180ms cubic-bezier(.23, 1, .32, 1), opacity 180ms ease;\n        }\n        [data-article-headings] .article-heading__index {\n          transition: color 180ms ease, opacity 180ms ease;\n        }\n        @media (hover: hover) and (pointer: fine) {\n          [data-article-headings] .article-heading:hover .article-heading__rule {\n            opacity: 1;\n            transform: scaleX(1);\n          }\n          [data-article-headings] .article-heading:hover .article-heading__index {\n            color: var(--article-accent);\n            opacity: 1;\n          }\n        }\n        @media (prefers-reduced-motion: reduce) {\n          [data-article-headings] .article-heading__rule,\n          [data-article-headings] .article-heading__index {\n            transition-duration: 0ms;\n          }\n        }\n      `}</style>\n      <div\n        className=\"w-full overflow-hidden rounded-[1.25rem] border px-5 py-5 shadow-[0_24px_80px_rgba(0,0,0,.16)] sm:px-8 sm:py-7\"\n        style={\n          {\n            \"--article-accent\": mode === \"dark\" ? \"#ec8b52\" : \"#bd5723\",\n            color: mode === \"dark\" ? \"#edf2ef\" : \"#17221d\",\n            background: mode === \"dark\" ? \"#090c0b\" : \"#eef2ef\",\n            borderColor:\n              mode === \"dark\" ? \"rgba(255,255,255,.12)\" : \"rgba(20,35,29,.16)\",\n          } as React.CSSProperties\n        }\n      >\n        <header\n          className=\"mb-2 flex items-center justify-between border-b pb-4 font-mono text-[9px] tracking-[0.2em] uppercase sm:text-[10px]\"\n          style={{\n            color: mode === \"dark\" ? \"#819089\" : \"#5f6f67\",\n            borderColor:\n              mode === \"dark\" ? \"rgba(255,255,255,.11)\" : \"rgba(20,35,29,.14)\",\n          }}\n        >\n          <span>Field notes / Index</span>\n          <span>{String(entries.length).padStart(2, \"0\")} entries</span>\n        </header>\n\n        <ol className=\"divide-y\" style={{ borderColor: \"currentColor\" }}>\n          {entries.map((entry, index) => {\n            const resolvedIndex =\n              entry.index ?? String(index + 1).padStart(2, \"0\")\n            return (\n              <li\n                key={`${resolvedIndex}-${entry.title}`}\n                className=\"article-heading relative\"\n                style={{\n                  borderColor:\n                    mode === \"dark\"\n                      ? \"rgba(255,255,255,.1)\"\n                      : \"rgba(20,35,29,.13)\",\n                }}\n              >\n                <article className=\"grid grid-cols-[2rem_minmax(0,1fr)] gap-3 py-6 sm:grid-cols-[3rem_minmax(0,1fr)_auto] sm:items-end sm:gap-5 sm:py-8\">\n                  <span\n                    className=\"article-heading__index self-start font-mono text-[10px] tracking-[0.16em]\"\n                    style={{ color: mode === \"dark\" ? \"#6f7f77\" : \"#65746d\" }}\n                    aria-hidden=\"true\"\n                  >\n                    {resolvedIndex}\n                  </span>\n                  <h2 className=\"min-w-0 font-heading text-xl leading-[1.05] font-medium tracking-[-0.025em] sm:text-3xl lg:text-4xl\">\n                    <span\n                      data-article-heading-text=\"\"\n                      data-value={entry.title}\n                      aria-hidden=\"true\"\n                    >\n                      {entry.title}\n                    </span>\n                    <span className=\"sr-only\">{entry.title}</span>\n                  </h2>\n                  {entry.meta ? (\n                    <span\n                      className=\"col-start-2 font-mono text-[8px] tracking-[0.13em] whitespace-nowrap uppercase sm:col-start-auto sm:pb-1 sm:text-[9px]\"\n                      style={{ color: mode === \"dark\" ? \"#77867f\" : \"#617068\" }}\n                    >\n                      {entry.meta}\n                    </span>\n                  ) : null}\n                </article>\n                <span\n                  aria-hidden=\"true\"\n                  className=\"article-heading__rule absolute inset-x-0 bottom-0 h-px opacity-70\"\n                  style={{ background: \"var(--article-accent)\" }}\n                />\n              </li>\n            )\n          })}\n        </ol>\n      </div>\n    </section>\n  )\n}\n",
      "type": "registry:ui",
      "target": "src/components/effects/article-headings.tsx"
    }
  ],
  "meta": {
    "collection": "motion-effects",
    "license": "MIT",
    "source": "https://github.com/MengTo/threeui",
    "sourceRevision": "fbc9b3d61b0ef4b2e93b42e4fffa617ca277429b"
  },
  "type": "registry:ui"
}