{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "curtain-tab-diagonal",
  "title": "Curtain Tab Diagonal",
  "description": "A full-bleed route curtain modeled on a real theatrical tab (tableau/opera-drape) curtain: the rigging where a line of rings runs on the diagonal from a panel's center-meeting bottom corner up to a fixed tie-off roughly a third of the way up the wing side, so hauling it draws that corner UP AND OUT along a curved diagonal while the header stays pinned, gathering the fabric into a swag before the whole tied-back panel retracts offscreen; a different axis of motion from curtain-traveler-draw's horizontal parting on a corded track.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/loud/curtain-tab-diagonal/component.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useId, useLayoutEffect, useRef, useState } from \"react\";\n\n// CurtainTabDiagonal — a full-bleed route curtain modeled on a real\n// theatrical TAB (tableau/opera-drape) curtain, not a traveler. A traveler\n// parts on a corded track, sideways. A tab curtain instead sews a line of\n// rings on the diagonal from the panel's center-meeting bottom corner up to\n// a fixed tie-off point roughly a third of the way up the panel on the wing\n// side; hauling that line draws ONLY that corner up and out toward the\n// tie-off, along a curved path, while the wing-side bottom corner and the\n// entire top edge never move — the rest of the hem interpolates between the\n// two, so the panel gathers into a genuine diagonal swag rather than a\n// uniform squash. Once the swag is fully gathered against its tie-off, the\n// whole panel retracts offscreen (a plain translateX, same clearance\n// distance as a traveler) — lift-then-slide, not a pure slide, and a\n// different choreography axis from curtain-traveler-draw's horizontal\n// corded parting throughout.\n//\n// Geometry: each panel is built from PLEATS+1 boundary x-positions (the\n// same cursor-accumulated widths a traveler uses). Boundary j owns a\n// \"reach\" r_j — 0 at the wing edge (never moves), 1 at the center-meeting\n// edge (travels the full arc to the tie-off) — and every pleat quad is a\n// path whose top two vertices are pinned at y=0 and whose bottom two\n// vertices are that boundary's lifted point. The lift itself is a\n// quadratic Bezier from the boundary's rest position to a per-boundary\n// target that is r_j of the way toward the tie-off, through a control point\n// bowed toward the viewer — so the corner's path is a real arc, and because\n// the Bezier parameter itself runs through an overshoot easing, the corner\n// briefly overshoots its tie-off and settles, the hoist-rope-going-taut\n// bounce. An opaque --ns-muted envelope path sits behind the pleat quads in\n// each group so the gradients' partially-transparent bands composite over\n// fabric, never over the page.\n//\n// Pleats are shaded path geometry exactly like the traveler build: a\n// --foreground-tinted shadow gradient and a --background-tinted highlight\n// gradient per fold, layered as partial opacity over the opaque --ns-muted\n// fabric body, hard-stopped at the crease. (--border is a separator token,\n// ~1.1:1 against --background in light theme — unusable as fabric ink; the\n// fold's form lives in value against --foreground/--background instead.)\n//\n// The idle sway is unconditional — even fully closed, every pleat's crease\n// line breathes ±8% of its own gradient-space width on a slow ~4s sine,\n// independently phase-offset per fold (not a whole-panel opacity pulse).\n//\n// Colors come only from --ns-muted (fabric body), --foreground (shadow\n// side) and --background (highlight side, plus the backing \"blocked page\"\n// rect), read once via getComputedStyle(document.documentElement) and\n// re-read on a MutationObserver watching documentElement's class — no hex\n// or rgb() literals anywhere in the draw code. --ns-accent never touches\n// the fabric; it only tints the optional \"skip curtain\" trigger button.\n\nexport interface CurtainTabDiagonalProps {\n  /** Called once the haul-open animation (or reduced-motion hard cut) completes. */\n  onOpenComplete?: () => void;\n  /** Freezes the idle sway and any in-flight haul at its current frame. */\n  paused?: boolean;\n  className?: string;\n}\n\ntype Tokens = {\n  muted: string;\n  foreground: string;\n  background: string;\n};\n\ntype Pt = { x: number; y: number };\n\nconst VW = 1600;\nconst VH = 900;\nconst CENTER = VW / 2;\nconst OVERLAP = 150; // px each panel's inner edge extends past center at rest\nconst OUTSET = 220; // px each panel's outer edge extends past the viewBox at rest\nconst OPEN_TX = VW + OUTSET + 120; // translateX distance to clear the panel fully offscreen\nconst PLEATS = 10;\nconst DRAW_MS = 760;\n// Fraction of DRAW_MS spent gathering the corner to its tie-off in place,\n// before the retract-offscreen slide begins.\nconst GATHER_FRAC = 0.55;\n// The tie-off point sits roughly a third of the way up the panel: the\n// bottom-inner corner rises by VH/3, ending at y = (2/3)*VH.\nconst ANCHOR_Y = VH * (2 / 3);\nconst ANCHOR_INSET = 40; // px in from the wing edge\nconst LEFT_ANCHOR_X = -OUTSET + ANCHOR_INSET;\nconst RIGHT_ANCHOR_X = VW + OUTSET - ANCHOR_INSET;\nconst BOW_MAX = 90; // px the arc's control point bows toward the viewer at full reach\nconst BREATH_PERIOD_MS = 4000;\nconst BREATH_AMP = 0.08; // ±8% of a pleat's own gradient-space width\nconst TRACK_HALF_W = 150;\nconst TRACK_Y = 20;\nconst TRACK_H = 10;\n// The gate target: a marker centered on the stage, at mid-height, well\n// below the rod. Both panels cover it at rest (their envelopes span the\n// full VH height across the full combined width); once gathered and\n// retracted fully offscreen, nothing paints over it. Unlike the rod\n// (fixed at TRACK_Y, never covered by fabric that only hangs below it),\n// this is genuinely occluded-then-exposed.\nconst MARKER_W = 160;\nconst MARKER_H = 48;\nconst MARKER_Y = VH * 0.5;\n\nconst LEFT_X0 = -OUTSET;\nconst LEFT_X1 = CENTER + OVERLAP;\nconst RIGHT_X0 = CENTER - OVERLAP;\nconst RIGHT_X1 = VW + OUTSET;\n\nfunction clamp01(t: number): number {\n  return Math.max(0, Math.min(1, t));\n}\n\nfunction lerp(a: number, b: number, t: number): number {\n  return a + (b - a) * t;\n}\n\nfunction easeOutBack(t: number): number {\n  // The hoist-rope overshoot as the tab line goes taut — f(1) = 1 exactly,\n  // so it settles back to the resting gathered position.\n  const c1 = 0.42;\n  const c3 = c1 + 1;\n  const p = t - 1;\n  return 1 + c3 * p * p * p + c1 * p * p;\n}\n\nfunction easeInOutCubic(t: number): number {\n  return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;\n}\n\n/** The bottom-inner (center-meeting) corner travels the full arc to the\n *  tie-off; the bottom-outer (wing) corner never moves; the rest of the hem\n *  interpolates by reach r, quadratic-Bezier'd through a viewer-bowed\n *  control point so the path is a real curve, not a straight diagonal. The\n *  Bezier parameter t runs through an overshoot easing, so a boundary with\n *  r > 0 briefly overshoots its target and settles — the taut-rope bounce. */\nfunction liftBoundary(x0: number, r: number, anchorX: number, t: number): Pt {\n  if (r <= 0) return { x: x0, y: VH };\n  const endX = lerp(x0, anchorX, r);\n  const endY = lerp(VH, ANCHOR_Y, r);\n  const midX = (x0 + endX) / 2;\n  const midY = (VH + endY) / 2 - BOW_MAX * r;\n  const omt = 1 - t;\n  return {\n    x: omt * omt * x0 + 2 * omt * t * midX + t * t * endX,\n    y: omt * omt * VH + 2 * omt * t * midY + t * t * endY,\n  };\n}\n\n/** Per-pleat proportional widths for one panel: narrower toward the wing\n *  (tie-off) end, wider toward the center-meeting edge; contrast sharpens\n *  as the panel gathers (fabric bunching tighter toward the tie-off point\n *  under the haul). */\nfunction pleatWeights(side: \"left\" | \"right\", gather: number): number[] {\n  const out: number[] = [];\n  for (let i = 0; i < PLEATS; i++) {\n    const frac = i / (PLEATS - 1); // 0 (leftmost) .. 1 (rightmost)\n    const gatherLocal = side === \"left\" ? frac : 1 - frac; // 0 = at wing/tie-off, 1 = at center\n    out.push(Math.max(0.3, 1 - gather * 0.55 + gather * 1.1 * gatherLocal));\n  }\n  return out;\n}\n\n/** PLEATS+1 boundary x-positions from x0 to x1, plus each boundary's reach\n *  r (0 at the wing edge, 1 at the center-meeting edge — reversed for the\n *  right panel, whose wing edge is x1). */\nfunction layoutBoundaries(side: \"left\" | \"right\", x0: number, x1: number, gather: number) {\n  const weights = pleatWeights(side, gather);\n  const sum = weights.reduce((a, b) => a + b, 0);\n  const panelW = x1 - x0;\n  const xs: number[] = [x0];\n  let cursor = x0;\n  for (const w of weights) {\n    cursor += (w / sum) * panelW;\n    xs.push(cursor);\n  }\n  return xs.map((x, j) => ({ x, r: side === \"left\" ? j / PLEATS : 1 - j / PLEATS }));\n}\n\ntype StopRefs = {\n  base0: SVGStopElement | null;\n  hiIn: SVGStopElement | null;\n  hiPeak: SVGStopElement | null;\n  shPeak: SVGStopElement | null;\n  shOut: SVGStopElement | null;\n  base1: SVGStopElement | null;\n};\n\nfunction emptyStopRefs(): StopRefs {\n  return { base0: null, hiIn: null, hiPeak: null, shPeak: null, shOut: null, base1: null };\n}\n\nexport function CurtainTabDiagonal({ onOpenComplete, paused = false, className = \"\" }: CurtainTabDiagonalProps) {\n  const uid = useId().replace(/[:]/g, \"\");\n  const rootRef = useRef<HTMLDivElement | null>(null);\n  const leftGroupRef = useRef<SVGGElement | null>(null);\n  const rightGroupRef = useRef<SVGGElement | null>(null);\n  const leftPathRefs = useRef<(SVGPathElement | null)[]>([]);\n  const rightPathRefs = useRef<(SVGPathElement | null)[]>([]);\n  const leftEnvelopeRef = useRef<SVGPathElement | null>(null);\n  const rightEnvelopeRef = useRef<SVGPathElement | null>(null);\n  const leftStopRefs = useRef<StopRefs[]>(Array.from({ length: PLEATS }, emptyStopRefs));\n  const rightStopRefs = useRef<StopRefs[]>(Array.from({ length: PLEATS }, emptyStopRefs));\n  const trackRef = useRef<SVGRectElement | null>(null);\n  const markerRef = useRef<SVGRectElement | null>(null);\n  const bgRectRef = useRef<SVGRectElement | null>(null);\n\n  // No hardcoded fallback palette: a hex default here would be the light-\n  // theme value baked in, so a dark-theme visitor could see one wrong-\n  // polarity frame before the real read lands. Null until derive() runs,\n  // and derive() runs in a layout effect (before the browser's first\n  // paint), not a passive effect, so that frame never actually happens.\n  const tokensRef = useRef<Tokens | null>(null);\n\n  const [open, setOpen] = useState(false);\n  const [opening, setOpening] = useState(false);\n  const [reduced, setReduced] = useState(false);\n  const [mounted, setMounted] = useState(false);\n\n  const drawStartRef = useRef<number | null>(null);\n  const mountTimeRef = useRef<number>(0);\n  const elapsedRef = useRef<number>(0);\n  const lastTsRef = useRef<number>(0);\n  const rafRef = useRef<number | undefined>(undefined);\n  const pausedRef = useRef(paused);\n  const openRef = useRef(false);\n  const openingRef = useRef(false);\n\n  useEffect(() => {\n    pausedRef.current = paused;\n  }, [paused]);\n\n  useEffect(() => {\n    setMounted(true);\n    const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    setReduced(mq.matches);\n    const onChange = () => setReduced(mq.matches);\n    mq.addEventListener(\"change\", onChange);\n    return () => mq.removeEventListener(\"change\", onChange);\n  }, []);\n\n  // --- token read + re-read on theme flip ------------------------------\n  const applyStaticColors = () => {\n    const t = tokensRef.current;\n    if (!t) return;\n    // --border is a separator token (~1.1:1 against --background in light\n    // theme) — unusable as fabric ink. The fold's form is modelled in VALUE\n    // against the --ns-muted body instead: shadow shades toward\n    // --foreground, highlight toward --background, both layered as partial\n    // opacity over the opaque muted base so it reads in both polarities.\n    if (trackRef.current) trackRef.current.setAttribute(\"fill\", t.muted);\n    if (markerRef.current) markerRef.current.setAttribute(\"fill\", t.muted);\n    if (bgRectRef.current) bgRectRef.current.setAttribute(\"fill\", t.background);\n    if (leftEnvelopeRef.current) leftEnvelopeRef.current.setAttribute(\"fill\", t.muted);\n    if (rightEnvelopeRef.current) rightEnvelopeRef.current.setAttribute(\"fill\", t.muted);\n    for (const refs of [...leftStopRefs.current, ...rightStopRefs.current]) {\n      refs.base0?.setAttribute(\"stop-color\", t.muted);\n      refs.base1?.setAttribute(\"stop-color\", t.muted);\n      refs.hiIn?.setAttribute(\"stop-color\", t.background);\n      refs.hiPeak?.setAttribute(\"stop-color\", t.background);\n      refs.shPeak?.setAttribute(\"stop-color\", t.foreground);\n      refs.shOut?.setAttribute(\"stop-color\", t.foreground);\n    }\n  };\n\n  // useLayoutEffect, not useEffect: runs before the browser's first paint,\n  // so the fabric never has a frame to render in the wrong theme's colors\n  // (or the SVG's black initial fill) before the token read lands — no\n  // hardcoded fallback palette needed at all.\n  useLayoutEffect(() => {\n    const derive = () => {\n      const cs = getComputedStyle(document.documentElement);\n      const read = (name: string) => cs.getPropertyValue(name).trim();\n      tokensRef.current = {\n        muted: read(\"--ns-muted\"),\n        foreground: read(\"--foreground\"),\n        background: read(\"--background\"),\n      };\n      applyStaticColors();\n    };\n    derive();\n    const mo = new MutationObserver(derive);\n    mo.observe(document.documentElement, { attributes: true, attributeFilter: [\"class\"] });\n    return () => mo.disconnect();\n  }, [mounted]);\n\n  const paintPath = (el: SVGPathElement | null, xa: number, xb: number, pa: Pt, pb: Pt) => {\n    if (!el) return;\n    el.setAttribute(\"d\", `M ${xa} 0 L ${xb} 0 L ${pb.x} ${pb.y} L ${pa.x} ${pa.y} Z`);\n  };\n\n  const paintEnvelope = (el: SVGPathElement | null, bx0: number, bx1: number, pts: Pt[]) => {\n    if (!el) return;\n    const rev = [...pts].reverse().map((p) => `L ${p.x} ${p.y}`).join(\" \");\n    el.setAttribute(\"d\", `M ${bx0} 0 L ${bx1} 0 ${rev} Z`);\n  };\n\n  // --- draw one frame (refs-only, no React state on the hot path) -------\n  const draw = (elapsedMs: number) => {\n    const breathPhaseBase = (elapsedMs / BREATH_PERIOD_MS) * Math.PI * 2;\n\n    let gatherRaw = 0; // 0 = closed, 1 = corner fully hauled to the tie-off point\n    let slideT = 0; // 0 = in place, 1 = swag fully retracted offscreen\n    if (drawStartRef.current !== null) {\n      const frac = clamp01((elapsedMs - drawStartRef.current) / DRAW_MS);\n      gatherRaw = clamp01(frac / GATHER_FRAC);\n      const slideRaw = clamp01((frac - GATHER_FRAC) / (1 - GATHER_FRAC));\n      slideT = easeInOutCubic(slideRaw);\n      if (frac >= 1 && !openRef.current) {\n        openRef.current = true;\n        openingRef.current = false;\n        setOpen(true);\n        setOpening(false);\n        onOpenComplete?.();\n      }\n    }\n    // Overshoot lives in the Bezier parameter itself: a boundary with\n    // reach > 0 briefly travels past its tie-off target and settles.\n    const bezierT = easeOutBack(gatherRaw);\n\n    const leftBoundaries = layoutBoundaries(\"left\", LEFT_X0, LEFT_X1, gatherRaw);\n    const rightBoundaries = layoutBoundaries(\"right\", RIGHT_X0, RIGHT_X1, gatherRaw);\n\n    const paintPanel = (\n      side: \"left\" | \"right\",\n      boundaries: { x: number; r: number }[],\n      anchorX: number,\n      pathRefs: (SVGPathElement | null)[],\n      envelopeRef: SVGPathElement | null,\n      stopRefs: StopRefs[],\n      groupEl: SVGGElement | null,\n      tx: number,\n    ) => {\n      if (groupEl) groupEl.setAttribute(\"transform\", `translate(${tx} 0)`);\n      const lifted = boundaries.map((b) => liftBoundary(b.x, b.r, anchorX, bezierT));\n      paintEnvelope(envelopeRef, boundaries[0].x, boundaries[PLEATS].x, lifted);\n      for (let i = 0; i < PLEATS; i++) {\n        paintPath(pathRefs[i], boundaries[i].x, boundaries[i + 1].x, lifted[i], lifted[i + 1]);\n        const phase = breathPhaseBase + i * 0.37 + (side === \"right\" ? 1.7 : 0);\n        const crease = 0.5 + BREATH_AMP * Math.sin(phase);\n        const refs = stopRefs[i];\n        refs?.hiIn?.setAttribute(\"offset\", String(crease - 0.2));\n        refs?.hiPeak?.setAttribute(\"offset\", String(crease - 0.02));\n        refs?.shPeak?.setAttribute(\"offset\", String(crease + 0.02));\n        refs?.shOut?.setAttribute(\"offset\", String(crease + 0.2));\n      }\n    };\n\n    paintPanel(\"left\", leftBoundaries, LEFT_ANCHOR_X, leftPathRefs.current, leftEnvelopeRef.current, leftStopRefs.current, leftGroupRef.current, -OPEN_TX * slideT);\n    paintPanel(\"right\", rightBoundaries, RIGHT_ANCHOR_X, rightPathRefs.current, rightEnvelopeRef.current, rightStopRefs.current, rightGroupRef.current, OPEN_TX * slideT);\n  };\n\n  // --- reduced motion: one static peak-amplitude frame, no rAF ----------\n  useEffect(() => {\n    if (!mounted || !reduced) return;\n    mountTimeRef.current = 0;\n    // Freeze at a quarter-period so the breathing sine sits at its peak\n    // (+8%), not the flat t=0 zero-crossing — pleats read as real fabric.\n    // Only paints the closed frame; handleTrigger paints the open frame\n    // directly on click (hard cut, no tween, no re-run of this effect).\n    draw(BREATH_PERIOD_MS / 4);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [mounted, reduced]);\n\n  // --- normal continuous rAF loop: unconditional idle sway + haul --------\n  useEffect(() => {\n    if (!mounted || reduced) return;\n    mountTimeRef.current = performance.now();\n    lastTsRef.current = mountTimeRef.current;\n    elapsedRef.current = 0;\n\n    const loop = (ts: number) => {\n      if (!pausedRef.current) {\n        elapsedRef.current += ts - lastTsRef.current;\n        draw(elapsedRef.current);\n      }\n      lastTsRef.current = ts;\n      rafRef.current = requestAnimationFrame(loop);\n    };\n    rafRef.current = requestAnimationFrame(loop);\n    return () => {\n      if (rafRef.current !== undefined) cancelAnimationFrame(rafRef.current);\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [mounted, reduced]);\n\n  const handleTrigger = () => {\n    if (openRef.current || openingRef.current) return;\n    if (reduced) {\n      // Hard cut, no tween: jump straight to the fully-gathered, fully-\n      // retracted geometry (bezierT = 1 exactly — no overshoot on a cut).\n      const leftBoundaries = layoutBoundaries(\"left\", LEFT_X0, LEFT_X1, 1);\n      const rightBoundaries = layoutBoundaries(\"right\", RIGHT_X0, RIGHT_X1, 1);\n      if (leftGroupRef.current) leftGroupRef.current.setAttribute(\"transform\", `translate(${-OPEN_TX} 0)`);\n      if (rightGroupRef.current) rightGroupRef.current.setAttribute(\"transform\", `translate(${OPEN_TX} 0)`);\n      const liftedLeft = leftBoundaries.map((b) => liftBoundary(b.x, b.r, LEFT_ANCHOR_X, 1));\n      const liftedRight = rightBoundaries.map((b) => liftBoundary(b.x, b.r, RIGHT_ANCHOR_X, 1));\n      paintEnvelope(leftEnvelopeRef.current, leftBoundaries[0].x, leftBoundaries[PLEATS].x, liftedLeft);\n      paintEnvelope(rightEnvelopeRef.current, rightBoundaries[0].x, rightBoundaries[PLEATS].x, liftedRight);\n      for (let i = 0; i < PLEATS; i++) {\n        paintPath(leftPathRefs.current[i], leftBoundaries[i].x, leftBoundaries[i + 1].x, liftedLeft[i], liftedLeft[i + 1]);\n        paintPath(rightPathRefs.current[i], rightBoundaries[i].x, rightBoundaries[i + 1].x, liftedRight[i], liftedRight[i + 1]);\n      }\n      openRef.current = true;\n      setOpen(true);\n      onOpenComplete?.();\n      return;\n    }\n    openingRef.current = true;\n    setOpening(true);\n    drawStartRef.current = elapsedRef.current;\n  };\n\n  const gid = (side: string, i: number) => `${uid}-pleat-${side}-${i}`;\n\n  const renderGradients = (side: \"left\" | \"right\") =>\n    Array.from({ length: PLEATS }, (_, i) => (\n      <linearGradient key={gid(side, i)} id={gid(side, i)} x1=\"0\" y1=\"0\" x2=\"1\" y2=\"0\">\n        <stop\n          offset=\"0\"\n          ref={(el) => {\n            (side === \"left\" ? leftStopRefs : rightStopRefs).current[i].base0 = el;\n          }}\n        />\n        <stop\n          offset=\"0.3\"\n          stopOpacity=\"0\"\n          ref={(el) => {\n            (side === \"left\" ? leftStopRefs : rightStopRefs).current[i].hiIn = el;\n          }}\n        />\n        <stop\n          offset=\"0.48\"\n          stopOpacity=\"0.55\"\n          ref={(el) => {\n            (side === \"left\" ? leftStopRefs : rightStopRefs).current[i].hiPeak = el;\n          }}\n        />\n        <stop\n          offset=\"0.52\"\n          stopOpacity=\"0.5\"\n          ref={(el) => {\n            (side === \"left\" ? leftStopRefs : rightStopRefs).current[i].shPeak = el;\n          }}\n        />\n        <stop\n          offset=\"0.7\"\n          stopOpacity=\"0\"\n          ref={(el) => {\n            (side === \"left\" ? leftStopRefs : rightStopRefs).current[i].shOut = el;\n          }}\n        />\n        <stop\n          offset=\"1\"\n          ref={(el) => {\n            (side === \"left\" ? leftStopRefs : rightStopRefs).current[i].base1 = el;\n          }}\n        />\n      </linearGradient>\n    ));\n\n  const renderPleatPaths = (side: \"left\" | \"right\") =>\n    Array.from({ length: PLEATS }, (_, i) => (\n      <path\n        key={i}\n        ref={(el) => {\n          (side === \"left\" ? leftPathRefs : rightPathRefs).current[i] = el;\n        }}\n        fill={`url(#${gid(side, i)})`}\n      />\n    ));\n\n  const showButton = !open;\n\n  return (\n    <div\n      ref={rootRef}\n      className={`absolute inset-0 overflow-hidden ${className}`}\n      role={open ? undefined : \"status\"}\n      aria-live={open ? undefined : \"polite\"}\n      aria-hidden={open ? \"true\" : undefined}\n    >\n      {!open ? <span className=\"sr-only\">Loading</span> : null}\n      <svg viewBox={`0 0 ${VW} ${VH}`} preserveAspectRatio=\"none\" className=\"block h-full w-full\" aria-hidden=\"true\" focusable=\"false\">\n        <defs>\n          {renderGradients(\"left\")}\n          {renderGradients(\"right\")}\n        </defs>\n        <rect ref={bgRectRef} x={0} y={0} width={VW} height={VH} />\n        <rect ref={trackRef} x={CENTER - TRACK_HALF_W} y={TRACK_Y} width={TRACK_HALF_W * 2} height={TRACK_H} rx={2} />\n        <rect\n          data-curtain-open\n          ref={markerRef}\n          x={CENTER - MARKER_W / 2}\n          y={MARKER_Y - MARKER_H / 2}\n          width={MARKER_W}\n          height={MARKER_H}\n          rx={2}\n        />\n        <g ref={leftGroupRef}>\n          <path ref={leftEnvelopeRef} />\n          {renderPleatPaths(\"left\")}\n        </g>\n        <g ref={rightGroupRef}>\n          <path ref={rightEnvelopeRef} />\n          {renderPleatPaths(\"right\")}\n        </g>\n      </svg>\n      {showButton ? (\n        <button\n          type=\"button\"\n          data-curtain-trigger\n          onClick={handleTrigger}\n          disabled={opening}\n          className=\"absolute bottom-6 left-1/2 -translate-x-1/2 inline-flex items-center rounded-sm bg-ns-accent px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-ns-accent-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ns-accent disabled:opacity-60 disabled:pointer-events-none\"\n        >\n          Skip curtain\n        </button>\n      ) : null}\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/curtain-tab-diagonal.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ns-muted": "var(--ns-muted)",
      "color-ns-accent": "var(--ns-accent)",
      "color-ns-accent-hover": "var(--ns-accent-hover)"
    },
    "light": {
      "ns-muted": "#4d4d4d",
      "ns-accent": "#006bff",
      "ns-accent-hover": "#0059d1"
    },
    "dark": {
      "ns-muted": "#8f8f8f"
    }
  },
  "meta": {
    "collection": "loud",
    "tags": [
      "preloader",
      "curtain",
      "route",
      "svg",
      "physics",
      "loading",
      "reveal"
    ],
    "instruction": "Build <CurtainTabDiagonal onOpenComplete? paused? className?> as a full-bleed route curtain modeled on a real theatrical TAB (tableau/opera-drape) curtain, distinct from a traveler curtain's horizontal corded parting. Two overlapping drape panels meet and overlap at center, full-bleed, when closed, each built from PLEATS+1 boundary points across its width with ~10 vertical sewn pleats between them. REAL TAB RIGGING, LOCALIZED TO ONE CORNER: a line of rings runs diagonally from each panel's center-meeting bottom corner up to a fixed tie-off point roughly a third of the way up the panel on its wing side. Hauling the component's own built-in 'Skip curtain' trigger (data-curtain-trigger) draws ONLY that bottom-inner corner up and out toward the tie-off; the bottom-outer (wing-side) corner and the entire top/header edge never move. Every intermediate boundary point interpolates by its own 'reach' (0 at the wing edge, 1 at the center-meeting edge) toward the tie-off, each along its own quadratic-Bezier arc through a control point bowed toward the viewer — never a straight diagonal, and never a uniform squash of the whole hem. The Bezier's own time parameter runs through a hoist-rope overshoot easing, so as the corner nears the tie-off it briefly overshoots past it and settles, the rope-going-taut bounce. Once the corner is fully gathered against its tie-off, the whole bunched swag then retracts fully offscreen via a plain translateX on the panel's SVG group, clearing the page — a lift-then-slide two-phase haul (the slide begins only once the gather is complete), not a pure slide, and a different choreography axis from curtain-traveler-draw's horizontal parting throughout. Pleats compress tighter toward the wing/tie-off end as the gather progresses, matching real fabric bunching toward the pull point. SVG, NOT CANVAS: pleats are shaded path quads (a --foreground-tinted shadow gradient and a --background-tinted highlight gradient per fold, each layered as partial opacity over an opaque --ns-muted envelope path sitting behind them in the same group, so the gradients' transparent bands composite over fabric, never over the page) and each panel moves as one SVG group on a plain translateX — no per-pixel field, so SVG is the cheap substrate. PALETTE: fabric body in --ns-muted, pleat shadow/highlight gradients shade toward --foreground and --background respectively (never --border, a separator token at ~1.1:1 contrast against --background in light theme and effectively invisible as fabric ink), blocked page area in --background — --ns-accent never touches the fabric, reserved only for the built-in skip/dismiss button's interaction chrome. Colors are read once via getComputedStyle(document.documentElement) and re-read on a MutationObserver watching documentElement's class — no hex or rgb() literals anywhere in the draw code. IDLE IS UNCONDITIONAL: even fully closed, every pleat's crease line breathes independently, its gradient stop offsets sliding back and forth across ±8% of the pleat's own width on a slow ~4s sine with a per-pleat phase offset — the closed state is never static, and fold lines genuinely shift position frame to frame rather than a whole-panel opacity or scale pulse, which is what the no-autoplay verifier grades. Because the interesting state (hauled open) is non-resting, a gate descriptor (openBy the trigger button, expect a track-rail element that sits behind the panels in paint order — covered while closed, exposed and hittable once the panels have gathered and retracted clear) triggers it for its own screenshot. prefers-reduced-motion holds a single mid-sway frame at peak fold-depth amplitude, where pleats read as three-dimensional fabric, and cuts hard (no tween, no rAF) between closed and open on trigger rather than tweening. role=status/aria-live=polite announces a loading state while closed; the whole overlay goes aria-hidden once fully open so the revealed route becomes the real focus target, and the trigger button unmounts at that point rather than staying focusable behind an aria-hidden ancestor."
  },
  "type": "registry:ui"
}