{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"container-scroll","type":"registry:ui","title":"ContainerScroll","description":"Scroll-driven aperture: the card opens from a bright seam as it enters the viewport while its content settles into focus and the title blurs away, with no 3D tilt","author":"fancy-ui <https://fancy-ui.rama.app>","categories":["layout"],"registryDependencies":["https://fancy-ui.rama.app/r/internals.json","https://fancy-ui.rama.app/r/internals-dom.json","https://fancy-ui.rama.app/r/internals-motion.json","https://fancy-ui.rama.app/r/utils.json"],"files":[{"path":"react/src/components/container-scroll/ContainerScroll.tsx","content":"\"use client\";\n\nimport { useRef, useState } from \"react\";\nimport type { CSSProperties, ReactNode } from \"react\";\nimport { useIsomorphicLayoutEffect } from \"../../internals/dom/ssr\";\nimport { useReducedMotion } from \"../../internals/motion/media-query\";\nimport { rafThrottle } from \"../../internals/motion/raf\";\nimport { cn } from \"../../utils\";\nimport \"./container-scroll.css\";\n\n/**\n * ContainerScroll - a scroll-driven aperture\n *\n * The card starts shut: a slim horizontal slit with a bright seam of light\n * across its middle. As the section scrolls into view the slit opens\n * vertically to the full card, the seam splits into two lips that ride the\n * opening edges and fade, the content settles from a slight zoom into\n * focus, and the title above blurs away to hand the stage to the card.\n * No 3D tilt, no perspective.\n */\nexport interface ContainerScrollProps {\n\t/** Additional CSS classes on the section */\n\tclassName?: string;\n\t/** Title shown above the card (blurs and fades as the card opens) */\n\ttitleContent?: ReactNode;\n\t/** Content revealed inside the card */\n\tcardContent?: ReactNode;\n\t/** Seam colour (any CSS colour). Defaults to a soft blue. */\n\taccent?: string;\n\t/** Second seam tint, blended towards the seam ends. Defaults to a soft lilac. */\n\taccentSecondary?: string;\n}\n\n/**\n * Raw scroll progress of the card track, measured at its centre (where the\n * slit is): 0 while the slit is still in the bottom 8% of the viewport,\n * 1 once it has risen to just above the middle of the viewport.\n */\nexport function apertureProgress(top: number, height: number, viewport: number): number {\n\tif (viewport <= 0) return 1;\n\tconst centre = top + height / 2;\n\tconst p = (viewport * 0.92 - centre) / (viewport * 0.4);\n\treturn p < 0 ? 0 : p > 1 ? 1 : p;\n}\n\n/** Smoothstep: a gentle start and a slow settle, so the opening reads as a lens, not a shutter. */\nexport function apertureEase(p: number): number {\n\tconst t = p < 0 ? 0 : p > 1 ? 1 : p;\n\treturn t * t * (3 - 2 * t);\n}\n\nexport function ContainerScroll({\n\tclassName = \"\",\n\ttitleContent,\n\tcardContent,\n\taccent,\n\taccentSecondary,\n}: ContainerScrollProps) {\n\tconst reduced = useReducedMotion();\n\tconst trackRef = useRef<HTMLDivElement>(null);\n\tconst [raw, setRaw] = useState(0);\n\n\tconst progress = reduced ? 1 : raw;\n\tconst open = apertureEase(progress);\n\n\tconst styleVars: Record<string, string> = {\n\t\t\"--cs-progress\": progress.toFixed(4),\n\t\t\"--cs-open\": open.toFixed(4),\n\t};\n\tif (accent) styleVars[\"--cs-accent\"] = accent;\n\tif (accentSecondary) styleVars[\"--cs-accent-2\"] = accentSecondary;\n\n\t// A layout effect, not a passive one: the Svelte effect measures before the\n\t// browser paints, so a card already in view never flashes one shut frame.\n\tuseIsomorphicLayoutEffect(() => {\n\t\tconst track = trackRef.current;\n\t\tif (!track || reduced) return;\n\n\t\tfunction measure() {\n\t\t\tif (!track) return;\n\t\t\tconst rect = track.getBoundingClientRect();\n\t\t\tsetRaw(apertureProgress(rect.top, rect.height, window.innerHeight));\n\t\t}\n\n\t\tconst update = rafThrottle(measure);\n\t\tmeasure();\n\t\t// Capture phase so the card also tracks scrolling inside a nested\n\t\t// scroll container, not only the window.\n\t\tconst opts = { capture: true, passive: true } as const;\n\t\twindow.addEventListener(\"scroll\", update, opts);\n\t\twindow.addEventListener(\"resize\", update, { passive: true });\n\t\treturn () => {\n\t\t\tupdate.cancel();\n\t\t\twindow.removeEventListener(\"scroll\", update, opts);\n\t\t\twindow.removeEventListener(\"resize\", update);\n\t\t};\n\t}, [reduced]);\n\n\treturn (\n\t\t<div\n\t\t\tclassName={cn(\n\t\t\t\t\"cs-root relative flex h-[48rem] w-full items-start justify-center p-2 md:h-[64rem] md:p-10\",\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\tstyle={styleVars as CSSProperties}\n\t\t\tdata-reduced-motion={reduced ? \"\" : undefined}\n\t\t>\n\t\t\t<div className=\"relative w-full py-10 md:py-16\">\n\t\t\t\t{/* Title */}\n\t\t\t\t<div className=\"cs-title mx-auto max-w-5xl text-center\">{titleContent}</div>\n\n\t\t\t\t{/* Card track (measured; never transformed) */}\n\t\t\t\t<div ref={trackRef} className=\"cs-track relative mx-auto mt-10 w-full max-w-5xl md:mt-12\">\n\t\t\t\t\t<div className=\"cs-stage relative aspect-[4/3] w-full sm:aspect-[16/10]\">\n\t\t\t\t\t\t{/* Ambient shadow: lives outside the clip so the aperture cannot cut it */}\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tclassName=\"cs-shadow pointer-events-none absolute inset-0\"\n\t\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\t></div>\n\n\t\t\t\t\t\t{/* Outer frame, clipped by the aperture */}\n\t\t\t\t\t\t<div className=\"cs-card absolute inset-0 p-1.5 md:p-2\">\n\t\t\t\t\t\t\t<div className=\"cs-surface relative size-full overflow-hidden\">\n\t\t\t\t\t\t\t\t<div className=\"cs-content size-full\">{cardContent}</div>\n\t\t\t\t\t\t\t\t{/* Veil: the content emerges from shadow as light gets in */}\n\t\t\t\t\t\t\t\t<div\n\t\t\t\t\t\t\t\t\tclassName=\"cs-veil pointer-events-none absolute inset-0\"\n\t\t\t\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\t\t\t></div>\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t</div>\n\n\t\t\t\t\t\t{/* Light: two lips riding the opening edges, and the seam at the centre */}\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tclassName=\"cs-lip cs-lip-top pointer-events-none absolute\"\n\t\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\t></div>\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tclassName=\"cs-lip cs-lip-bottom pointer-events-none absolute\"\n\t\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\t></div>\n\t\t\t\t\t\t<div className=\"cs-seam pointer-events-none absolute\" aria-hidden=\"true\">\n\t\t\t\t\t\t\t<span className=\"cs-seam-haze\"></span>\n\t\t\t\t\t\t\t<span className=\"cs-seam-glow\"></span>\n\t\t\t\t\t\t\t<span className=\"cs-seam-line\"></span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n","type":"registry:ui","target":"@ui/fancy/components/container-scroll/ContainerScroll.tsx"},{"path":"react/src/components/container-scroll/container-scroll.css","content":"/*\n * ContainerScroll — a scroll-driven aperture.\n * Every rule is anchored on `.cs-root` (the Svelte source's own root class):\n * the Svelte compiler scoped these selectors, plain CSS is global.\n */\n\n.cs-root {\n\t--_accent: var(--cs-accent, #8fb2ff);\n\t--_accent-2: var(--cs-accent-2, #c3b1ff);\n\t--_core: color-mix(in oklab, var(--_accent) 45%, white);\n\t--_radius: 22px;\n\t--_glow: 75%;\n\t--_frame: #f4f4f5;\n\t--_frame-line: rgba(0, 0, 0, 0.08);\n\t--_surface: #fafafa;\n\t--_surface-line: rgba(0, 0, 0, 0.07);\n\t--_shadow:\n\t\t0 1px 2px rgba(0, 0, 0, 0.04), 0 12px 28px -12px rgba(0, 0, 0, 0.12),\n\t\t0 40px 80px -40px rgba(0, 0, 0, 0.22);\n\t/* 1 → closed slit (48% top + 48% bottom), 0 → fully open */\n\t--_shut: calc(1 - var(--cs-open, 0));\n}\n\n.dark .cs-root {\n\t--_glow: 60%;\n\t--_frame: #0f0f10;\n\t--_frame-line: rgba(255, 255, 255, 0.08);\n\t--_surface: #0b0b0c;\n\t--_surface-line: rgba(255, 255, 255, 0.07);\n\t--_shadow:\n\t\t0 1px 0 rgba(255, 255, 255, 0.03) inset, 0 20px 50px -24px rgba(0, 0, 0, 0.9),\n\t\t0 60px 120px -60px rgba(0, 0, 0, 0.8);\n}\n\n/* Title: hands the stage over by going out of focus */\n.cs-root .cs-title {\n\t/* starts only once the aperture is a quarter open */\n\t--_fade: clamp(0, (var(--cs-progress, 0) - 0.25) / 0.75, 1);\n\topacity: calc(1 - var(--_fade) * 0.85);\n\tfilter: blur(calc(var(--_fade) * 6px));\n\ttransform: scale(calc(1 - var(--_fade) * 0.02));\n\ttransform-origin: 50% 100%;\n}\n\n.cs-root .cs-stage {\n\ttransform: translateY(calc(var(--_shut) * 24px));\n}\n\n.cs-root .cs-shadow {\n\tborder-radius: var(--_radius);\n\tbox-shadow: var(--_shadow);\n\t/* follows the opening so it never outlines the still-hidden card */\n\ttransform: scaleY(calc(1 - var(--_shut) * 0.96));\n\topacity: calc(var(--cs-open, 0) * var(--cs-open, 0));\n}\n\n.cs-root .cs-card {\n\tborder-radius: var(--_radius);\n\tbackground: var(--_frame);\n\tborder: 1px solid var(--_frame-line);\n\tclip-path: inset(calc(var(--_shut) * 48%) 0 calc(var(--_shut) * 48%) 0 round var(--_radius));\n}\n\n.cs-root .cs-surface {\n\tborder-radius: calc(var(--_radius) - 7px);\n\tbackground: var(--_surface);\n\tbox-shadow: 0 0 0 1px var(--_surface-line);\n}\n\n.cs-root .cs-content {\n\ttransform: scale(calc(1 + var(--_shut) * 0.08));\n\ttransform-origin: 50% 50%;\n}\n\n/* ---- Light ---------------------------------------------------------- */\n\n.cs-root .cs-seam {\n\tleft: 4%;\n\tright: 4%;\n\ttop: 50%;\n\theight: 0;\n\topacity: calc(1 - var(--cs-progress, 0) * 3);\n}\n\n.cs-root .cs-seam-line,\n.cs-root .cs-seam-glow,\n.cs-root .cs-seam-haze {\n\tposition: absolute;\n\tleft: 0;\n\tright: 0;\n\ttop: 0;\n\tborder-radius: 999px;\n}\n\n.cs-root .cs-seam-line {\n\theight: 1px;\n\ttransform: translateY(-50%);\n\tbackground: linear-gradient(\n\t\t90deg,\n\t\ttransparent,\n\t\tcolor-mix(in oklab, var(--_accent-2) 70%, transparent) 14%,\n\t\tvar(--_accent) 34%,\n\t\tvar(--_core) 50%,\n\t\tvar(--_accent) 66%,\n\t\tcolor-mix(in oklab, var(--_accent-2) 70%, transparent) 86%,\n\t\ttransparent\n\t);\n\tbox-shadow: 0 0 6px 0 color-mix(in oklab, var(--_accent) 60%, transparent);\n}\n\n.cs-root .cs-seam-glow {\n\theight: 28px;\n\ttransform: translateY(-50%);\n\tbackground: radial-gradient(\n\t\t50% 50% at 50% 50%,\n\t\tcolor-mix(in oklab, var(--_accent) var(--_glow), transparent),\n\t\tcolor-mix(in oklab, var(--_accent-2) 25%, transparent) 60%,\n\t\ttransparent 100%\n\t);\n\tfilter: blur(10px);\n}\n\n/* a wide, faint horizon of light spilling past the slit */\n.cs-root .cs-seam-haze {\n\tleft: -6%;\n\tright: -6%;\n\theight: 90px;\n\ttransform: translateY(-50%);\n\tbackground: radial-gradient(\n\t\t50% 50% at 50% 50%,\n\t\tcolor-mix(in oklab, var(--_accent) calc(var(--_glow) * 0.45), transparent),\n\t\ttransparent 70%\n\t);\n\tfilter: blur(18px);\n}\n\n.cs-root .cs-veil {\n\tbackground: var(--_surface);\n\topacity: calc(var(--_shut) * var(--_shut) * 0.75);\n}\n\n.cs-root .cs-lip {\n\tleft: 3%;\n\tright: 3%;\n\theight: 1px;\n\tbackground: linear-gradient(\n\t\t90deg,\n\t\ttransparent,\n\t\tcolor-mix(in oklab, var(--_accent-2) 55%, transparent) 20%,\n\t\tcolor-mix(in oklab, var(--_accent) 80%, white) 50%,\n\t\tcolor-mix(in oklab, var(--_accent-2) 55%, transparent) 80%,\n\t\ttransparent\n\t);\n\tbox-shadow: 0 0 10px 0 color-mix(in oklab, var(--_accent) 45%, transparent);\n\topacity: var(--_shut);\n}\n\n.cs-root .cs-lip-top {\n\ttop: calc(var(--_shut) * 48%);\n}\n\n.cs-root .cs-lip-bottom {\n\tbottom: calc(var(--_shut) * 48%);\n}\n\n/* Light theme: a white core vanishes on a pale page, so deepen the seam */\nhtml:not(.dark) .cs-root {\n\t--_core: color-mix(in oklab, var(--_accent) 75%, white);\n}\n\n/* Reduced motion: a still frame — card open, title sharp, no light */\n.cs-root[data-reduced-motion] {\n\t--_shut: 0;\n}\n.cs-root[data-reduced-motion] .cs-title {\n\topacity: 1;\n\tfilter: none;\n\ttransform: none;\n}\n.cs-root[data-reduced-motion] .cs-seam,\n.cs-root[data-reduced-motion] .cs-lip {\n\tdisplay: none;\n}\n\n@media (prefers-reduced-motion: reduce) {\n\t.cs-root {\n\t\t--_shut: 0;\n\t}\n\t.cs-root .cs-title {\n\t\topacity: 1;\n\t\tfilter: none;\n\t\ttransform: none;\n\t}\n\t.cs-root .cs-seam,\n\t.cs-root .cs-lip {\n\t\tdisplay: none;\n\t}\n\t.cs-root .cs-shadow {\n\t\topacity: 1;\n\t}\n}\n","type":"registry:ui","target":"@ui/fancy/components/container-scroll/container-scroll.css"},{"path":"react/src/components/container-scroll/index.ts","content":"\"use client\";\n\nexport { ContainerScroll } from \"./ContainerScroll\";\nexport type { ContainerScrollProps } from \"./ContainerScroll\";\n","type":"registry:ui","target":"@ui/fancy/components/container-scroll/index.ts"}],"meta":{"license":"MIT","package":"fancy-ui-react","version":"0.7.0","source":"https://fancy-ui.rama.app/docs/components/container-scroll?framework=react"}}