{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"flow-art","type":"registry:component","title":"Flow Art","description":"Colour-block sections that swing in over each other as you scroll.","author":"DiMaac (HankTheTank) <https://dimaac.com>","dependencies":["gsap","@gsap/react"],"files":[{"path":"registry/dimaac/flow-art/flow-art.tsx","type":"registry:component","target":"components/dimaac/flow-art.tsx","content":"\"use client\";\n\nimport { useRef, type CSSProperties, type ReactNode } from \"react\";\nimport { gsap } from \"gsap\";\nimport { ScrollTrigger } from \"gsap/ScrollTrigger\";\nimport { useGSAP } from \"@gsap/react\";\n\n// Big colour-block sections that flow into each other. As you scroll, the next section swings in\n// from a tilted angle (like a page being turned) while the section before it stays pinned underneath.\n// Two small pieces:\n//   <FlowArt>      wraps everything and runs the animation\n//   <FlowSection>  one coloured panel. Put any content inside it.\n\nif (typeof window !== \"undefined\") gsap.registerPlugin(useGSAP, ScrollTrigger);\n\nexport type FlowArtProps = {\n  /** The sections, in order. Use <FlowSection> for each one. */\n  children: ReactNode;\n  /** How far each section is tilted before it swings in, in degrees. */\n  angle?: number;\n  /** Which bottom corner the section swings around: the left one or the right one. */\n  from?: \"left\" | \"right\";\n  /** Where on the screen the swing ends, as a fraction of the screen height from the top. 0.25 means a quarter of the way down. */\n  settle?: number;\n  /** Keep every section pinned in place while the next one covers it. Off, they scroll away normally. */\n  pin?: boolean;\n  /** How long the swing takes to catch up with the scrollbar, in seconds. 0 locks it to the scrollbar. */\n  smoothing?: number;\n  /** The scrolling element. Leave it out to use the page; give it a selector or an element for an inner scroller. */\n  scroller?: string | HTMLElement | Window;\n  /** Extra classes for the outer wrapper. */\n  className?: string;\n};\n\nexport function FlowArt({ children, angle = 30, from = \"left\", settle = 0.25, pin = true, smoothing = 0, scroller, className = \"\" }: FlowArtProps) {\n  const rootRef = useRef<HTMLDivElement>(null);\n\n  useGSAP(\n    () => {\n      const root = rootRef.current;\n      if (!root) return;\n\n      const mm = gsap.matchMedia();\n\n      // Reduced motion: no tilting and no pinning. The sections are just a plain coloured page.\n      mm.add(\"(prefers-reduced-motion: no-preference)\", () => {\n        const sections = gsap.utils.toArray<HTMLElement>(\"[data-flow-section]\", root);\n        // Swinging round the right corner means turning the other way\n        const direction = from === \"right\" ? -1 : 1;\n        const origin = from === \"right\" ? \"bottom right\" : \"bottom left\";\n\n        sections.forEach((section, index) => {\n          const panel = section.querySelector<HTMLElement>(\"[data-flow-panel]\");\n          if (!panel) return;\n\n          // The first section is simply there. Every other one swings in while it climbs the screen.\n          // scrub ties the swing to the scrollbar, so it swings back out when you scroll up.\n          if (index > 0) {\n            gsap.fromTo(\n              panel,\n              { rotation: angle * direction, transformOrigin: origin },\n              {\n                rotation: 0,\n                ease: \"none\",\n                force3D: true,\n                scrollTrigger: { trigger: section, scroller, start: \"top bottom\", end: `top ${settle * 100}%`, scrub: smoothing > 0 ? smoothing : true },\n              },\n            );\n          }\n\n          // Hold the section still, with the next one sliding over the top of it. A section taller than\n          // the screen pins by its bottom edge, so everything in it has been scrolled past first.\n          if (pin && index < sections.length - 1) {\n            ScrollTrigger.create({ trigger: section, scroller, start: \"bottom bottom\", end: \"bottom top\", pin: true, pinSpacing: false });\n          }\n        });\n\n        // Fonts and pictures arriving late change the heights, so measure the pins again.\n        // A short delay turns a burst of changes into one measurement.\n        let measure: gsap.core.Tween | undefined;\n        const measureSoon = () => {\n          measure?.kill();\n          measure = gsap.delayedCall(0.2, () => ScrollTrigger.refresh());\n        };\n        const observer = new ResizeObserver(measureSoon);\n        // Only the wrapper: a pinned section changes its own box while it sticks, which must not re-trigger this\n        observer.observe(root);\n        let cancelled = false;\n        document.fonts?.ready.then(() => {\n          if (!cancelled) measureSoon();\n        });\n\n        return () => {\n          cancelled = true;\n          measure?.kill();\n          observer.disconnect();\n        };\n      });\n\n      return () => mm.revert();\n    },\n    { scope: rootRef, dependencies: [angle, from, settle, pin, smoothing, scroller], revertOnUpdate: true },\n  );\n\n  return (\n    <div ref={rootRef} className={`relative w-full overflow-x-clip ${className}`}>\n      {children}\n    </div>\n  );\n}\n\nexport type FlowSectionProps = {\n  /** What the section shows. */\n  children: ReactNode;\n  /** Extra classes for the coloured panel: the background, text colour and padding go here. */\n  className?: string;\n  /** Inline styles for the panel, e.g. a background colour. */\n  style?: CSSProperties;\n};\n\n/** One coloured panel. It is the panel (not the section around it) that swings in, so the corners cut into the section below. */\nexport function FlowSection({ children, className = \"\", style }: FlowSectionProps) {\n  return (\n    <section data-flow-section className=\"relative w-full overflow-hidden\">\n      <div\n        data-flow-panel\n        className={`relative flex min-h-[100svh] w-full flex-col justify-between gap-6 px-[max(1.25rem,4vw)] pt-[max(2rem,4vw)] pb-[max(1.5rem,4vw)] ${className}`}\n        style={{ transformOrigin: \"bottom left\", willChange: \"transform\", ...style }}\n      >\n        {children}\n      </div>\n    </section>\n  );\n}\n"}],"categories":["scroll","scrolltrigger","pin","scrub","sections","editorial"],"docs":"import { FlowArt } from \"@/components/dimaac/flow-art\". Give its parent a size: it fills it. Docs, playground and props: https://ui.dimaac.com/components/flow-art","meta":{"tier":"free","preview":"https://ui.dimaac.com/components/flow-art","credits":[{"by":"rena","url":"https://www.lummi.ai/photo/modern-sculpture-display-y1he7","what":"Modern Sculpture Display","source":"Lummi","licence":"Lummi License"},{"by":"Ricardo Matos","url":"https://www.lummi.ai/photo/surreal-3d-planet-scene-ofhhw","what":"Surreal 3D Planet Scene","source":"Lummi","licence":"Lummi License"},{"by":"Dona Mara","url":"https://www.lummi.ai/photo/abstract-sculptural-objects-flp-3","what":"Abstract Sculptural Objects","source":"Lummi","licence":"Lummi License"},{"by":"SHIHO","url":"https://www.lummi.ai/photo/vibrant-cylindrical-arrangement-pj9pp","what":"Vibrant Cylindrical Arrangement","source":"Lummi","licence":"Lummi License"},{"by":"rena","url":"https://www.lummi.ai/photo/abstract-wavy-design-bypvi","what":"Abstract Wavy Design","source":"Lummi","licence":"Lummi License"},{"by":"marisol","url":"https://www.lummi.ai/photo/glossy-pink-sphere-45si5","what":"Glossy Pink Sphere","source":"Lummi","licence":"Lummi License"}]}}