{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "custom",
  "description": "Layout-aware custom renderers from the `animate-text` catalog that cannot be",
  "dependencies": [
    "@gsap/react",
    "gsap"
  ],
  "files": [
    {
      "path": "registry/new-york/text/animate-text/custom.tsx",
      "content": "\"use client\";\n\n/**\n * Layout-aware custom renderers from the `animate-text` catalog that cannot be\n * expressed as a plain generic stagger:\n *\n *   - SharedSlideText  → `shared-slide-opacity-stage` (short-slide-right)\n *   - KineticCenterText → `kinetic-center-build`\n *   - KineticStackText  → `kinetic-top-build` (short-slide-down)\n *\n * Timings below are the website-scaled values (source × 0.72) taken from each\n * effect's `showcase.timing` block. Kinetic build x/y values are raw renderer\n * pixels and are intentionally NOT scaled by the vertical-travel multiplier.\n */\n\nimport { useRef } from \"react\";\nimport gsap from \"gsap\";\nimport { useGSAP } from \"@gsap/react\";\nimport { toEase } from \"./engine\";\n\ntype Anim = gsap.core.Tween | gsap.core.Timeline;\n\nconst mix = (a: number, b: number, t: number) => a + (b - a) * t;\n\ntype WordState = { x: number; y: number; scale: number; blur: number; opacity: number };\n\nfunction writeWord(el: HTMLElement, s: WordState) {\n  el.style.transform = `translate(-50%, -50%) translate3d(${s.x}px, ${s.y}px, 0) scale(${s.scale})`;\n  el.style.filter = `blur(${s.blur}px)`;\n  el.style.opacity = String(s.opacity);\n}\n\nfunction createAbsWord(word: string) {\n  const el = document.createElement(\"span\");\n  el.textContent = word;\n  el.style.position = \"absolute\";\n  el.style.left = \"50%\";\n  el.style.top = \"50%\";\n  el.style.whiteSpace = \"pre\";\n  el.style.opacity = \"0\";\n  el.style.willChange = \"transform, opacity, filter\";\n  el.style.transform = \"translate(-50%, -50%)\";\n  el.style.backfaceVisibility = \"hidden\";\n  return el;\n}\n\ntype KeyFrame = { offset: number } & WordState;\n\nconst SLIDE_SAMPLES = [\"Move with intent.\", \"Words glide across.\", \"Build the rhythm.\"];\n\nexport function SharedSlideText({ className }: { className?: string }) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const titleRef = useRef<HTMLSpanElement>(null);\n\n  useGSAP(\n    () => {\n      const title = titleRef.current;\n      if (!title) return;\n\n      let cancelled = false;\n      const timers: number[] = [];\n      const active: Anim[] = [];\n      const enterEase = toEase(\"cubic-bezier(0.2, 0.8, 0.2, 1)\");\n      const exitEase = toEase(\"cubic-bezier(0.4, 0, 0.2, 1)\");\n\n      const sleep = (ms: number) =>\n        new Promise<void>((resolve) => {\n          const t = window.setTimeout(resolve, ms);\n          timers.push(t);\n        });\n      const tween = (target: gsap.TweenTarget, vars: gsap.TweenVars) =>\n        new Promise<void>((resolve) => {\n          const tw = gsap.to(target, { ...vars, onComplete: () => resolve() });\n          active.push(tw);\n        });\n\n      const build = (text: string) => {\n        title.textContent = \"\";\n        const words: HTMLSpanElement[] = [];\n        for (const part of text.match(/(\\S+|\\s+)/g) ?? [text]) {\n          const span = document.createElement(\"span\");\n          span.textContent = part;\n          span.style.display = \"inline-block\";\n          span.style.whiteSpace = \"pre\";\n          title.appendChild(span);\n          if (/\\S/.test(part)) words.push(span);\n        }\n        return words;\n      };\n\n      const enter = async (words: HTMLSpanElement[]) => {\n        gsap.set(title, { x: -24, filter: \"blur(1.2px)\", opacity: 1 });\n        gsap.set(words, { opacity: 0 });\n        const wordTw = gsap.to(words, {\n          opacity: 1,\n          duration: 0.151,\n          ease: enterEase,\n          stagger: { each: 0.066, from: \"start\" },\n        });\n        active.push(wordTw);\n        await tween(title, { x: 0, filter: \"blur(0px)\", duration: 0.374, ease: enterEase });\n      };\n\n      (async () => {\n        await sleep(Math.random() * 400);\n        if (cancelled) return;\n        let i = 0;\n        await enter(build(SLIDE_SAMPLES[0]));\n        active.length = 0;\n        while (!cancelled) {\n          await sleep(550);\n          if (cancelled) break;\n          await tween(title, { x: 12, filter: \"blur(1px)\", opacity: 0, duration: 0.23, ease: exitEase });\n          active.length = 0;\n          if (cancelled) break;\n          i = (i + 1) % SLIDE_SAMPLES.length;\n          await enter(build(SLIDE_SAMPLES[i]));\n          active.length = 0;\n          if (cancelled) break;\n          await sleep(320);\n        }\n      })();\n\n      return () => {\n        cancelled = true;\n        timers.forEach((t) => clearTimeout(t));\n        active.forEach((a) => a.kill());\n        if (titleRef.current) titleRef.current.textContent = \"\";\n      };\n    },\n    { scope: containerRef }\n  );\n\n  return (\n    <div ref={containerRef} className={className} style={{ overflow: \"hidden\" }}>\n      <span ref={titleRef} style={{ display: \"inline-block\", willChange: \"transform, opacity, filter\" }} />\n    </div>\n  );\n}\n\nconst CENTER_PHRASES = [\n  [\"Words\", \"push\", \"left\"],\n  [\"Type\", \"locks\", \"center\"],\n  [\"Build\", \"the\", \"line\"],\n];\nconst CENTER = {\n  firstSec: 0.245,\n  pushSec: 0.31,\n  exitSec: 0.187,\n  holdMs: 706,\n  gapMs: 158,\n  offset: 88,\n  wordGap: 10,\n  firstY: 6,\n  entryScale: 0.992,\n  entryBlur: 3.5,\n  reflowBlur: 0.8,\n  exitY: -6,\n  exitBlur: 2.5,\n};\n\nexport function KineticCenterText({ className }: { className?: string }) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const hostRef = useRef<HTMLDivElement>(null);\n\n  useGSAP(\n    () => {\n      const host = hostRef.current;\n      if (!host) return;\n\n      let cancelled = false;\n      const timers: number[] = [];\n      const active: Anim[] = [];\n      const enterEase = toEase(\"cubic-bezier(0.2, 0.8, 0.2, 1)\");\n      const exitEase = toEase(\"cubic-bezier(0.4, 0, 0.2, 1)\");\n\n      const sleep = (ms: number) =>\n        new Promise<void>((resolve) => {\n          const t = window.setTimeout(resolve, ms);\n          timers.push(t);\n        });\n\n      const run = (\n        state: WordState,\n        el: HTMLElement,\n        frames: KeyFrame[],\n        totalSec: number,\n        ease: gsap.EaseFunction | string\n      ) => {\n        const write = () => writeWord(el, state);\n        Object.assign(state, frames[0]);\n        write();\n        const tl = gsap.timeline();\n        for (let k = 1; k < frames.length; k++) {\n          const dur = (frames[k].offset - frames[k - 1].offset) * totalSec;\n          tl.to(state, {\n            x: frames[k].x,\n            y: frames[k].y,\n            scale: frames[k].scale,\n            blur: frames[k].blur,\n            opacity: frames[k].opacity,\n            duration: dur,\n            ease,\n            onUpdate: write,\n          });\n        }\n        active.push(tl);\n        return new Promise<void>((resolve) => tl.eventCallback(\"onComplete\", resolve));\n      };\n\n      type Item = { el: HTMLElement; state: WordState; pos: number };\n\n      const buildPhrase = async (words: string[]) => {\n        host.textContent = \"\";\n        const items: Item[] = [];\n\n        for (let k = 0; k < words.length; k++) {\n          const el = createAbsWord(words[k]);\n          host.appendChild(el);\n\n          const widths = items.map((it) => it.el.offsetWidth);\n          widths.push(el.offsetWidth);\n          const count = widths.length;\n          const total = widths.reduce((a, b) => a + b, 0) + CENTER.wordGap * (count - 1);\n          let cursor = -total / 2;\n          const positions = widths.map((w) => {\n            const p = cursor + w / 2;\n            cursor += w + CENTER.wordGap;\n            return p;\n          });\n\n          const item: Item = { el, state: { x: 0, y: 0, scale: 1, blur: 0, opacity: 1 }, pos: 0 };\n          items.push(item);\n\n          if (k === 0) {\n            item.pos = positions[0];\n            await run(\n              item.state,\n              el,\n              [\n                { offset: 0, x: 0, y: CENTER.firstY, scale: CENTER.entryScale, blur: CENTER.entryBlur, opacity: 0 },\n                { offset: 0.58, x: 0, y: CENTER.firstY * 0.35, scale: 0.998, blur: CENTER.entryBlur * 0.45, opacity: 0.78 },\n                { offset: 1, x: 0, y: 0, scale: 1, blur: 0, opacity: 1 },\n              ],\n              CENTER.firstSec,\n              enterEase\n            );\n          } else {\n            const moves: Promise<void>[] = [];\n            for (let j = 0; j < items.length - 1; j++) {\n              const cur = items[j].pos;\n              const next = positions[j];\n              items[j].pos = next;\n              moves.push(\n                run(\n                  items[j].state,\n                  items[j].el,\n                  [\n                    { offset: 0, x: cur, y: 0, scale: 1, blur: 0, opacity: 1 },\n                    { offset: 0.52, x: mix(cur, next, 0.58), y: 0, scale: 1, blur: CENTER.reflowBlur, opacity: 1 },\n                    { offset: 1, x: next, y: 0, scale: 1, blur: 0, opacity: 1 },\n                  ],\n                  CENTER.pushSec,\n                  enterEase\n                )\n              );\n            }\n            const target = positions[positions.length - 1];\n            item.pos = target;\n            moves.push(\n              run(\n                item.state,\n                el,\n                [\n                  { offset: 0, x: target + CENTER.offset, y: 0, scale: CENTER.entryScale, blur: CENTER.entryBlur, opacity: 0 },\n                  { offset: 0.6, x: mix(target + CENTER.offset, target, 0.72), y: 0, scale: 0.998, blur: CENTER.entryBlur * 0.38, opacity: 0.84 },\n                  { offset: 1, x: target, y: 0, scale: 1, blur: 0, opacity: 1 },\n                ],\n                CENTER.pushSec,\n                enterEase\n              )\n            );\n            await Promise.all(moves);\n          }\n\n          items.forEach((it, idx) => {\n            it.pos = positions[idx];\n            it.state = { x: positions[idx], y: 0, scale: 1, blur: 0, opacity: 1 };\n            writeWord(it.el, it.state);\n          });\n          active.length = 0;\n          if (cancelled) return items;\n        }\n        return items;\n      };\n\n      const exitPhrase = async (items: Item[]) => {\n        const exits = items.map((it) =>\n          run(\n            it.state,\n            it.el,\n            [\n              { offset: 0, x: it.pos, y: 0, scale: 1, blur: 0, opacity: 1 },\n              { offset: 0.52, x: it.pos, y: CENTER.exitY * 0.45, scale: 1, blur: CENTER.exitBlur * 0.55, opacity: 0.62 },\n              { offset: 1, x: it.pos, y: CENTER.exitY, scale: 1, blur: CENTER.exitBlur, opacity: 0 },\n            ],\n            CENTER.exitSec,\n            exitEase\n          )\n        );\n        await Promise.all(exits);\n        active.length = 0;\n      };\n\n      (async () => {\n        await sleep(Math.random() * 400);\n        if (cancelled) return;\n        let i = 0;\n        while (!cancelled) {\n          const items = await buildPhrase(CENTER_PHRASES[i]);\n          if (cancelled) break;\n          await sleep(CENTER.holdMs);\n          if (cancelled) break;\n          await exitPhrase(items);\n          if (cancelled) break;\n          host.textContent = \"\";\n          await sleep(CENTER.gapMs);\n          i = (i + 1) % CENTER_PHRASES.length;\n        }\n      })();\n\n      return () => {\n        cancelled = true;\n        timers.forEach((t) => clearTimeout(t));\n        active.forEach((a) => a.kill());\n        if (hostRef.current) hostRef.current.textContent = \"\";\n      };\n    },\n    { scope: containerRef }\n  );\n\n  return (\n    <div ref={containerRef} className={className} style={{ position: \"relative\", height: \"3.5rem\" }}>\n      <div ref={hostRef} style={{ position: \"absolute\", inset: 0 }} />\n    </div>\n  );\n}\n\nconst STACK_PHRASES = [\n  [\"Drop\", \"into\", \"place\"],\n  [\"Words\", \"settle\", \"lower\"],\n  [\"Build\", \"from\", \"above\"],\n];\nconst STACK = {\n  firstSec: 0.259,\n  pushSec: 0.36,\n  exitSec: 0.23,\n  holdMs: 792,\n  gapMs: 130,\n  offset: -28,\n  lineGap: 12,\n  firstY: -14,\n  entryScale: 0.992,\n  entryBlur: 2.4,\n  reflowBlur: 0.7,\n  exitY: 10,\n  exitBlur: 1.2,\n};\n\nexport function KineticStackText({ className }: { className?: string }) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const hostRef = useRef<HTMLDivElement>(null);\n\n  useGSAP(\n    () => {\n      const host = hostRef.current;\n      if (!host) return;\n\n      let cancelled = false;\n      const timers: number[] = [];\n      const active: Anim[] = [];\n      const enterEase = toEase(\"cubic-bezier(0.2, 0.8, 0.2, 1)\");\n      const exitEase = toEase(\"cubic-bezier(0.4, 0, 0.2, 1)\");\n\n      const sleep = (ms: number) =>\n        new Promise<void>((resolve) => {\n          const t = window.setTimeout(resolve, ms);\n          timers.push(t);\n        });\n\n      const run = (\n        state: WordState,\n        el: HTMLElement,\n        frames: KeyFrame[],\n        totalSec: number,\n        ease: gsap.EaseFunction | string\n      ) => {\n        const write = () => writeWord(el, state);\n        Object.assign(state, frames[0]);\n        write();\n        const tl = gsap.timeline();\n        for (let k = 1; k < frames.length; k++) {\n          const dur = (frames[k].offset - frames[k - 1].offset) * totalSec;\n          tl.to(state, {\n            x: frames[k].x,\n            y: frames[k].y,\n            scale: frames[k].scale,\n            blur: frames[k].blur,\n            opacity: frames[k].opacity,\n            duration: dur,\n            ease,\n            onUpdate: write,\n          });\n        }\n        active.push(tl);\n        return new Promise<void>((resolve) => tl.eventCallback(\"onComplete\", resolve));\n      };\n\n      type Item = { el: HTMLElement; state: WordState; pos: number };\n\n      const buildPhrase = async (words: string[]) => {\n        host.textContent = \"\";\n        const items: Item[] = [];\n\n        for (let k = 0; k < words.length; k++) {\n          const el = createAbsWord(words[k]);\n          el.style.display = \"block\";\n          host.appendChild(el);\n\n          const heights = items.map((it) => it.el.offsetHeight);\n          heights.push(el.offsetHeight);\n          const count = heights.length;\n          const total = heights.reduce((a, b) => a + b, 0) + STACK.lineGap * (count - 1);\n          let cursor = -total / 2;\n          const positions = heights.map((h) => {\n            const p = cursor + h / 2;\n            cursor += h + STACK.lineGap;\n            return p;\n          });\n\n          const item: Item = { el, state: { x: 0, y: 0, scale: 1, blur: 0, opacity: 1 }, pos: 0 };\n          items.push(item);\n\n          if (k === 0) {\n            item.pos = positions[0];\n            await run(\n              item.state,\n              el,\n              [\n                { offset: 0, x: 0, y: STACK.firstY, scale: STACK.entryScale, blur: STACK.entryBlur, opacity: 0 },\n                { offset: 0.58, x: 0, y: STACK.firstY * 0.35, scale: 0.998, blur: STACK.entryBlur * 0.45, opacity: 0.78 },\n                { offset: 1, x: 0, y: 0, scale: 1, blur: 0, opacity: 1 },\n              ],\n              STACK.firstSec,\n              enterEase\n            );\n          } else {\n            const moves: Promise<void>[] = [];\n            for (let j = 0; j < items.length - 1; j++) {\n              const cur = items[j].pos;\n              const next = positions[j];\n              items[j].pos = next;\n              moves.push(\n                run(\n                  items[j].state,\n                  items[j].el,\n                  [\n                    { offset: 0, x: 0, y: cur, scale: 1, blur: 0, opacity: 1 },\n                    { offset: 0.52, x: 0, y: mix(cur, next, 0.58), scale: 1, blur: STACK.reflowBlur, opacity: 1 },\n                    { offset: 1, x: 0, y: next, scale: 1, blur: 0, opacity: 1 },\n                  ],\n                  STACK.pushSec,\n                  enterEase\n                )\n              );\n            }\n            const target = positions[positions.length - 1];\n            item.pos = target;\n            moves.push(\n              run(\n                item.state,\n                el,\n                [\n                  { offset: 0, x: 0, y: target + STACK.offset, scale: STACK.entryScale, blur: STACK.entryBlur, opacity: 0 },\n                  { offset: 0.6, x: 0, y: mix(target + STACK.offset, target, 0.72), scale: 0.998, blur: STACK.entryBlur * 0.38, opacity: 0.84 },\n                  { offset: 1, x: 0, y: target, scale: 1, blur: 0, opacity: 1 },\n                ],\n                STACK.pushSec,\n                enterEase\n              )\n            );\n            await Promise.all(moves);\n          }\n\n          items.forEach((it, idx) => {\n            it.pos = positions[idx];\n            it.state = { x: 0, y: positions[idx], scale: 1, blur: 0, opacity: 1 };\n            writeWord(it.el, it.state);\n          });\n          active.length = 0;\n          if (cancelled) return items;\n        }\n        return items;\n      };\n\n      const exitPhrase = async (items: Item[]) => {\n        const exits = items.map((it) =>\n          run(\n            it.state,\n            it.el,\n            [\n              { offset: 0, x: 0, y: it.pos, scale: 1, blur: 0, opacity: 1 },\n              { offset: 0.52, x: 0, y: it.pos + STACK.exitY * 0.45, scale: 1, blur: STACK.exitBlur * 0.55, opacity: 0.62 },\n              { offset: 1, x: 0, y: it.pos + STACK.exitY, scale: 1, blur: STACK.exitBlur, opacity: 0 },\n            ],\n            STACK.exitSec,\n            exitEase\n          )\n        );\n        await Promise.all(exits);\n        active.length = 0;\n      };\n\n      (async () => {\n        await sleep(Math.random() * 400);\n        if (cancelled) return;\n        let i = 0;\n        while (!cancelled) {\n          const items = await buildPhrase(STACK_PHRASES[i]);\n          if (cancelled) break;\n          await sleep(STACK.holdMs);\n          if (cancelled) break;\n          await exitPhrase(items);\n          if (cancelled) break;\n          host.textContent = \"\";\n          await sleep(STACK.gapMs);\n          i = (i + 1) % STACK_PHRASES.length;\n        }\n      })();\n\n      return () => {\n        cancelled = true;\n        timers.forEach((t) => clearTimeout(t));\n        active.forEach((a) => a.kill());\n        if (hostRef.current) hostRef.current.textContent = \"\";\n      };\n    },\n    { scope: containerRef }\n  );\n\n  return (\n    <div ref={containerRef} className={className} style={{ position: \"relative\", height: \"7rem\" }}>\n      <div ref={hostRef} style={{ position: \"absolute\", inset: 0 }} />\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/pixel-perfect/custom.tsx"
    }
  ],
  "type": "registry:block"
}