{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "data-flow",
  "title": "Data Flow",
  "author": "AlgorizeTech",
  "description": "A sources → core → outputs section: platform cards on the left feed one node in the middle, which fans out to result cards on the right, with curved connectors that draw in on first view and then carry a moving dash. Use it to explain a pipeline, an integration, or where an AI product's data comes from.",
  "dependencies": [],
  "registryDependencies": [
    "https://avaraui.com/r/apple-select.json",
    "https://avaraui.com/r/button.json",
    "https://avaraui.com/r/flowchart.json",
    "https://avaraui.com/r/half-stars.json",
    "https://avaraui.com/r/icons.json",
    "utils"
  ],
  "files": [
    {
      "path": "src/components/custom/blocks/DataFlowBlock.tsx",
      "content": "\"use client\";\n\nimport React, { useEffect, useRef, useState, useSyncExternalStore } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { AppleSelect } from \"./AppleSelectBlock\";\nimport { FlowCanvas, Flowchart, FlowConnection } from \"./FlowchartBlock\";\n\n// ==========================================\n// DATA FLOW\n// A \"sources → core → outputs\" section: cards on the left feed one node in\n// the middle, which fans out to cards on the right. The connectors are real\n// curves measured from the cards, so they hold at any width; they draw in\n// once when the section first scrolls into view and then carry a moving\n// dash. Every card is a named part, so the content is entirely yours.\n// ==========================================\n\ntype Point = { x: number; y: number };\n\nconst REDUCED_MOTION = \"(prefers-reduced-motion: reduce)\";\nconst subscribeReducedMotion = (onChange: () => void) => {\n  if (typeof window === \"undefined\" || !window.matchMedia) return () => {};\n  const mq = window.matchMedia(REDUCED_MOTION);\n  mq.addEventListener?.(\"change\", onChange);\n  return () => mq.removeEventListener?.(\"change\", onChange);\n};\n/** The visitor's reduced-motion setting; false while server-rendering. */\nfunction usePrefersReducedMotion() {\n  return useSyncExternalStore(\n    subscribeReducedMotion,\n    () => typeof window !== \"undefined\" && window.matchMedia?.(REDUCED_MOTION).matches === true,\n    () => false,\n  );\n}\n\n// GSAP's power3.out (sampled) and back.out(1.6) (exact), for Web Animations.\nconst POWER3_OUT =\n  \"linear(0, .1945, .3591, .4971, .6115, .7052, .7808, .8409, .8877, .9233, .9497, .9686, .9816, .9901, .9952, .998, .9994, .9999, 1, 1)\";\nconst BACK_OUT_1_6 = \"cubic-bezier(0.33333, 1.53333, 0.66667, 1)\";\n\nconst cardSurface =\n  \"rounded-2xl border border-border bg-card text-card-foreground shadow-[0_1px_2px_rgba(0,0,0,0.04),0_10px_28px_-14px_rgba(0,0,0,0.16)]\";\n\n// ------------------------------------------\n// Small pieces the cards are made of\n// ------------------------------------------\n\n/** A solid pill in the theme ink — counts and scores. */\nfunction Pill({ className, children, ...props }: React.HTMLAttributes<HTMLSpanElement>) {\n  return (\n    <span\n      className={cn(\n        \"inline-flex shrink-0 items-center rounded-full bg-foreground px-2.5 py-1 text-[10.5px] font-semibold leading-none tracking-tight text-background\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </span>\n  );\n}\n\n/** A hairline chip — attributes and facets. */\nfunction Tag({ className, children, ...props }: React.HTMLAttributes<HTMLSpanElement>) {\n  return (\n    <span\n      className={cn(\n        \"inline-flex items-center rounded-full border border-foreground/10 bg-card px-2.5 py-1 text-[10px] font-medium leading-none text-foreground/70\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </span>\n  );\n}\n\n/** A quiet grey row inside an output card. */\nfunction Row({ className, children, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n  return (\n    <div\n      className={cn(\n        \"flex items-center justify-between gap-3 rounded-lg bg-foreground/[0.04] px-3 py-2 text-[11px] font-medium text-foreground\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\n// ------------------------------------------\n// Source — a group of platforms feeding the core\n// ------------------------------------------\nexport interface DataFlowSourceItem {\n  label: string;\n  icon?: React.ReactNode;\n}\n\nexport interface DataFlowSourceProps extends React.HTMLAttributes<HTMLDivElement> {\n  title: string;\n  /** e.g. \"2 sources\" */\n  count?: string;\n  items: DataFlowSourceItem[];\n}\n\nfunction Source({ title, count, items, className, ...props }: DataFlowSourceProps) {\n  return (\n    <div data-flow=\"source\" className={cn(cardSurface, \"p-4\", className)} {...props}>\n      <div className=\"flex items-center justify-between gap-3\">\n        <p className=\"text-[10px] font-semibold uppercase tracking-[0.14em] text-muted-foreground\">{title}</p>\n        {count && <Pill>{count}</Pill>}\n      </div>\n      <ul className=\"mt-3 flex flex-wrap gap-2\">\n        {items.map((item) => (\n          <li\n            key={item.label}\n            className=\"inline-flex items-center gap-2 rounded-xl border border-border bg-card py-1.5 pl-1.5 pr-3.5 text-[13px] font-semibold text-foreground shadow-[0_1px_2px_rgba(0,0,0,0.05),0_4px_12px_-6px_rgba(0,0,0,0.12)]\"\n          >\n            {item.icon && <span className=\"grid h-6 w-6 shrink-0 place-items-center [&>svg]:h-full [&>svg]:w-full\">{item.icon}</span>}\n            {item.label}\n          </li>\n        ))}\n      </ul>\n    </div>\n  );\n}\n\n// ------------------------------------------\n// Core — the node in the middle\n// ------------------------------------------\nexport interface DataFlowCoreProps extends React.HTMLAttributes<HTMLDivElement> {\n  title: string;\n  subtitle?: string;\n}\n\nfunction Core({ title, subtitle, className, ...props }: DataFlowCoreProps) {\n  return (\n    <div\n      data-flow=\"core\"\n      className={cn(cardSurface, \"flex flex-col items-center justify-center px-6 py-9 text-center\", className)}\n      {...props}\n    >\n      <p className=\"text-[1.5rem] font-semibold leading-none tracking-tight text-foreground\">{title}</p>\n      {subtitle && (\n        <p className=\"mt-2.5 text-[9.5px] font-semibold uppercase tracking-[0.16em] text-muted-foreground\">{subtitle}</p>\n      )}\n    </div>\n  );\n}\n\n// ------------------------------------------\n// Output — a result card on the right\n// ------------------------------------------\nexport interface DataFlowOutputProps extends React.HTMLAttributes<HTMLDivElement> {\n  title: string;\n  /** e.g. \"3,432 niches\" */\n  badge?: string;\n  children?: React.ReactNode;\n}\n\nfunction Output({ title, badge, className, children, ...props }: DataFlowOutputProps) {\n  return (\n    <div data-flow=\"output\" className={cn(cardSurface, \"p-4\", className)} {...props}>\n      <div className=\"flex items-center justify-between gap-3\">\n        <p className=\"text-[13px] font-semibold tracking-tight text-foreground\">{title}</p>\n        {badge && <Pill>{badge}</Pill>}\n      </div>\n      {children && <div className=\"mt-2.5 space-y-2\">{children}</div>}\n    </div>\n  );\n}\n\n// ------------------------------------------\n// Root\n// ------------------------------------------\nexport interface DataFlowProps extends React.HTMLAttributes<HTMLElement> {\n  /** Small line over the heading. */\n  eyebrow?: React.ReactNode;\n  heading: React.ReactNode;\n  /** Paragraph under the heading. Phrases listed in `highlights` are selected in it, iOS-style. */\n  intro?: string;\n  /** Phrases from `intro` to select — each is matched once, in order of appearance. */\n  highlights?: string[];\n  /** Colour of the moving dashes and any accent inside; defaults to the theme's primary. */\n  accent?: string;\n  /** Play the reveal when the section enters the viewport (default) or immediately. */\n  reveal?: \"in-view\" | \"immediate\";\n  children: React.ReactNode;\n}\n\nconst isPart = (child: React.ReactNode, type: React.ElementType) =>\n  React.isValidElement(child) && child.type === type;\n\nfunction curve(from: Point, to: Point, vertical: boolean) {\n  if (vertical) {\n    const k = (to.y - from.y) / 2;\n    return `M ${from.x} ${from.y} C ${from.x} ${from.y + k}, ${to.x} ${to.y - k}, ${to.x} ${to.y}`;\n  }\n  const k = (to.x - from.x) / 2;\n  return `M ${from.x} ${from.y} C ${from.x + k} ${from.y}, ${to.x - k} ${to.y}, ${to.x} ${to.y}`;\n}\n\nexport function DataFlow({\n  eyebrow,\n  heading,\n  intro,\n  highlights = [],\n  accent,\n  reveal = \"in-view\",\n  className,\n  style,\n  children,\n  ...props\n}: DataFlowProps) {\n  const all = React.Children.toArray(children);\n  const sources = all.filter((c) => isPart(c, Source));\n  const core = all.find((c) => isPart(c, Core));\n  const outputs = all.filter((c) => isPart(c, Output));\n\n  const sectionRef = useRef<HTMLElement>(null);\n  const boardRef = useRef<HTMLDivElement>(null);\n  const [inView, setInView] = useState(reveal === \"immediate\");\n  const [linesReady, setLinesReady] = useState(false);\n  const reducedMotion = usePrefersReducedMotion();\n  // With reduced motion the lines are simply there once the section is in view.\n  const showLines = linesReady || (inView && reducedMotion);\n  const [paths, setPaths] = useState<string[]>([]);\n  const [size, setSize] = useState<{ w: number; h: number }>({ w: 0, h: 0 });\n\n  // Wait for the section before anything moves.\n  useEffect(() => {\n    if (inView) return;\n    const el = sectionRef.current;\n    if (!el || typeof IntersectionObserver === \"undefined\") {\n      setInView(true);\n      return;\n    }\n    const io = new IntersectionObserver(\n      (entries) => {\n        if (entries.some((e) => e.isIntersecting)) {\n          setInView(true);\n          io.disconnect();\n        }\n      },\n      { threshold: 0.25 },\n    );\n    io.observe(el);\n    return () => io.disconnect();\n  }, [inView]);\n\n  // Measure the cards and lay curves between them. Runs again whenever the\n  // board changes size, so the lines follow the layout.\n  useEffect(() => {\n    const board = boardRef.current;\n    if (!board) return;\n\n    const measure = () => {\n      const b = board.getBoundingClientRect();\n      const coreEl = board.querySelector<HTMLElement>('[data-flow=\"core\"]');\n      if (!coreEl) return;\n      const c = coreEl.getBoundingClientRect();\n      const rel = (r: DOMRect) => ({\n        left: r.left - b.left,\n        right: r.right - b.left,\n        top: r.top - b.top,\n        bottom: r.bottom - b.top,\n        cx: r.left - b.left + r.width / 2,\n        cy: r.top - b.top + r.height / 2,\n      });\n      const cr = rel(c);\n      const next: string[] = [];\n      const GAP = 6;\n\n      board.querySelectorAll<HTMLElement>('[data-flow=\"source\"]').forEach((el) => {\n        const r = rel(el.getBoundingClientRect());\n        // Side by side: leave the card's right edge for the core's left. Stacked: drop from its bottom.\n        const vertical = r.right > cr.left;\n        next.push(\n          vertical\n            ? curve({ x: r.cx, y: r.bottom + GAP }, { x: cr.cx, y: cr.top - GAP }, true)\n            : curve({ x: r.right + GAP, y: r.cy }, { x: cr.left - GAP, y: cr.cy }, false),\n        );\n      });\n      board.querySelectorAll<HTMLElement>('[data-flow=\"output\"]').forEach((el) => {\n        const r = rel(el.getBoundingClientRect());\n        const vertical = r.left < cr.right;\n        next.push(\n          vertical\n            ? curve({ x: cr.cx, y: cr.bottom + GAP }, { x: r.cx, y: r.top - GAP }, true)\n            : curve({ x: cr.right + GAP, y: cr.cy }, { x: r.left - GAP, y: r.cy }, false),\n        );\n      });\n      setPaths(next);\n      setSize({ w: b.width, h: b.height });\n    };\n\n    measure();\n    if (typeof ResizeObserver === \"undefined\") return;\n    const ro = new ResizeObserver(measure);\n    ro.observe(board);\n    return () => ro.disconnect();\n  }, [sources.length, outputs.length]);\n\n  // The one orchestrated moment: sources arrive, lines draw to the core, the\n  // core lands, lines draw on, outputs arrive. Holds still for reduced motion.\n  useEffect(() => {\n    if (!inView) return;\n    const board = boardRef.current;\n    if (!board) return;\n    const reduced =\n      typeof window !== \"undefined\" && window.matchMedia?.(\"(prefers-reduced-motion: reduce)\").matches;\n    const src = board.querySelectorAll<HTMLElement>('[data-flow=\"source\"]');\n    const coreEl = board.querySelector<HTMLElement>('[data-flow=\"core\"]');\n    const out = board.querySelectorAll<HTMLElement>('[data-flow=\"output\"]');\n    if (reduced) return;\n    // One timeline, as Web Animations sharing a start: each card waits out its\n    // delay already at its starting pose (`backwards`), then eases home.\n    const animations: Animation[] = [];\n    const play = (el: HTMLElement | null, from: Keyframe, delay: number, duration: number, easing: string) => {\n      if (!el || typeof el.animate !== \"function\") return;\n      animations.push(\n        el.animate([from, { opacity: 1, transform: \"none\" }], {\n          delay: delay * 1000,\n          duration: duration * 1000,\n          easing,\n          fill: \"backwards\",\n        }),\n      );\n    };\n    src.forEach((el, i) => play(el, { opacity: 0, transform: \"translateX(-28px)\" }, i * 0.09, 0.7, POWER3_OUT));\n    const lines = window.setTimeout(() => setLinesReady(true), 350);\n    play(coreEl, { opacity: 0, transform: \"scale(0.92)\" }, 0.75, 0.6, BACK_OUT_1_6);\n    out.forEach((el, i) => play(el, { opacity: 0, transform: \"translateX(28px)\" }, 1.35 + i * 0.09, 0.7, POWER3_OUT));\n    return () => {\n      window.clearTimeout(lines);\n      animations.forEach((a) => a.cancel());\n    };\n  }, [inView]);\n\n  const sourceCount = sources.length;\n\n  // Turn the highlight phrases into character ranges; a phrase that is not in\n  // the text is skipped rather than throwing.\n  const ranges: [number, number][] = [];\n  if (intro) {\n    let from = 0;\n    for (const phrase of highlights) {\n      const at = intro.indexOf(phrase, from);\n      if (at === -1) continue;\n      ranges.push([at, at + phrase.length]);\n      from = at + phrase.length;\n    }\n  }\n\n  return (\n    <section\n      ref={sectionRef}\n      className={cn(\"w-full bg-background text-foreground\", className)}\n      style={accent ? ({ ...style, \"--primary\": accent } as React.CSSProperties) : style}\n      {...props}\n    >\n      {(eyebrow || heading || intro) && (\n        <header className=\"mx-auto max-w-2xl px-6 pb-12 pt-14 text-center sm:pb-14 sm:pt-16\">\n          {eyebrow && (\n            <p className=\"text-[13px] font-medium uppercase tracking-[0.12em] text-muted-foreground\">{eyebrow}</p>\n          )}\n          <h2 className=\"mt-3 font-sans text-[2.25rem] font-medium leading-[1.05] tracking-[-0.03em] sm:text-[2.75rem]\">\n            {heading}\n          </h2>\n          {intro && (\n            <AppleSelect\n              text={intro}\n              selection={ranges}\n              animate={inView}\n              className=\"mx-auto mt-5 max-w-[38rem] text-[17px] leading-[1.85] text-foreground/65 [&_mark]:font-semibold [&_mark]:text-foreground\"\n            />\n          )}\n        </header>\n      )}\n\n      {/* A hairline with a soft shadow under it, so the board reads as a stage. */}\n      <div className=\"h-px w-full bg-border shadow-[0_6px_18px_-6px_rgba(0,0,0,0.12)]\" aria-hidden />\n\n      <div className=\"mx-auto max-w-7xl px-4 py-12 sm:px-8 sm:py-16\">\n        <Flowchart layout=\"fluid\" initialActivePath=\"flow\" color=\"primary\">\n          <div\n            ref={boardRef}\n            className=\"relative grid grid-cols-1 gap-y-3 lg:grid-cols-[minmax(0,1fr)_13rem_minmax(0,1fr)] lg:gap-x-12 lg:gap-y-0 xl:grid-cols-[minmax(0,1fr)_15.5rem_minmax(0,1fr)] xl:gap-x-24 2xl:gap-x-28\"\n          >\n            {size.w > 0 && showLines && (\n              <FlowCanvas\n                width={size.w}\n                height={size.h}\n                viewBox={`0 0 ${size.w} ${size.h}`}\n                preserveAspectRatio=\"none\"\n                // Under the cards: if a curve ever crosses one it disappears behind it.\n                className=\"z-0 hidden overflow-visible lg:block\"\n              >\n                {paths.map((d, i) => (\n                  <FlowConnection\n                    key={i}\n                    id=\"flow\"\n                    path={d}\n                    dashed=\"4 7\"\n                    flowDash=\"14 46\"\n                    glow={false}\n                    drawDuration={i < sourceCount ? 0.9 : 1.1}\n                    className=\"text-foreground/15\"\n                  />\n                ))}\n              </FlowCanvas>\n            )}\n\n            <div className=\"relative z-10 flex flex-col justify-center gap-4\">{sources}</div>\n\n            {/* Below lg the columns stack; short dashed spines stand in for the curves. */}\n            <div className=\"relative z-10 flex flex-col items-center justify-center gap-3\">\n              <span className=\"h-7 w-px border-l border-dashed border-foreground/25 lg:hidden\" aria-hidden />\n              <div className=\"w-full max-w-[15.5rem]\">{core}</div>\n              <span className=\"h-7 w-px border-l border-dashed border-foreground/25 lg:hidden\" aria-hidden />\n            </div>\n\n            <div className=\"relative z-10 flex flex-col justify-center gap-4\">{outputs}</div>\n          </div>\n        </Flowchart>\n      </div>\n    </section>\n  );\n}\n\nDataFlow.Source = Source;\nDataFlow.Core = Core;\nDataFlow.Output = Output;\nDataFlow.Pill = Pill;\nDataFlow.Tag = Tag;\nDataFlow.Row = Row;\n\nexport default DataFlow;\n",
      "type": "registry:component",
      "target": "components/custom/blocks/DataFlowBlock.tsx"
    }
  ],
  "docs": "Named export: DataFlow, with parts Source, Core, Output, Pill, Tag and Row. Composed from the flowchart and apple-select items, which install alongside it (`intro` + `highlights` render through AppleSelect); the docs example also uses half-stars and button inside its output cards. Connectors are measured from the cards at runtime (ResizeObserver), hidden below lg; `reveal` is in-view or immediate; `accent` pins the dash colour, otherwise it follows --primary. Wrap intro phrases in <mark> for black pills.",
  "categories": [
    "marketing",
    "data-display"
  ],
  "type": "registry:component"
}
