{"$schema":"https:\/\/ui.particle.academy\/schema\/registry-item.json","name":"marquee","type":"registry:ui","title":"Marquee","description":"Auto-scrolling ticker strip \u2014 seamless wrap, px\/s speed, opposing directions, fade edges, reduced-motion safe.","package":"react-fancy","dependencies":[],"registryDependencies":[],"files":[{"path":"components\/fancy\/marquee\/Marquee.tsx","content":"import {\n  Children,\n  Fragment,\n  forwardRef,\n  useEffect,\n  useImperativeHandle,\n  useRef,\n  useState,\n  type CSSProperties,\n  type ReactNode,\n} from \"react\";\nimport { cn } from \"..\/..\/utils\/cn\";\nimport type { MarqueeProps } from \".\/Marquee.types\";\n\nfunction toLength(value: string | number): string {\n  return typeof value === \"number\" ? `${value}px` : value;\n}\n\nexport const Marquee = forwardRef<HTMLDivElement, MarqueeProps>(\n  (\n    {\n      children,\n      items,\n      speed = 40,\n      duration,\n      direction = \"left\",\n      pauseOnHover = false,\n      paused = false,\n      gap = 40,\n      fade = true,\n      separator,\n      angle = 0,\n      decorative = true,\n      className,\n      style,\n      ...rest\n    },\n    ref,\n  ) => {\n    const rootRef = useRef<HTMLDivElement>(null);\n    const unitRef = useRef<HTMLDivElement>(null);\n    useImperativeHandle(ref, () => rootRef.current as HTMLDivElement);\n\n    \/\/ copies = how many times the item sequence repeats inside ONE loop copy,\n    \/\/ so short content still fills the strip edge-to-edge (no blank wrap).\n    const [copies, setCopies] = useState(1);\n    const [measuredDuration, setMeasuredDuration] = useState<number | null>(null);\n\n    \/\/ Measure one content unit (incl. its trailing gap) + the container:\n    \/\/  - auto-fill: repeat the unit until one loop copy covers the container\n    \/\/  - px\/s mode: derive the loop duration from the loop distance so the\n    \/\/    perceived speed stays constant regardless of content width.\n    \/\/ Browser-only (effect) \u2014 SSR renders one unit at the fallback pace.\n    useEffect(() => {\n      const root = rootRef.current;\n      const unit = unitRef.current;\n      if (!root || !unit || typeof ResizeObserver === \"undefined\") return;\n      const update = () => {\n        const unitWidth = unit.getBoundingClientRect().width;\n        const containerWidth = root.clientWidth;\n        if (unitWidth <= 0) return;\n        const nextCopies = Math.max(1, Math.ceil(containerWidth \/ unitWidth));\n        setCopies(nextCopies);\n        if (duration === undefined) {\n          setMeasuredDuration((unitWidth * nextCopies) \/ Math.max(speed, 1));\n        }\n      };\n      update();\n      const observer = new ResizeObserver(update);\n      observer.observe(root);\n      observer.observe(unit);\n      return () => observer.disconnect();\n    }, [duration, speed]);\n\n    const loopSeconds = duration ?? measuredDuration ?? 40;\n    const itemNodes: ReactNode[] = items ?? Children.toArray(children);\n    const hasSeparator = separator !== undefined && separator !== null;\n\n    \/\/ One unit = the item sequence once (separator after every item so the\n    \/\/ wrap point looks identical to any other joint).\n    const renderUnit = (duplicate: boolean, unitElRef?: React.Ref<HTMLDivElement>) => (\n      <div\n        ref={unitElRef}\n        className=\"fancy-marquee__unit\"\n        aria-hidden={duplicate || undefined}\n      >\n        {itemNodes.map((node, i) => (\n          <Fragment key={i}>\n            <div className=\"fancy-marquee__item\" data-react-fancy-marquee-item=\"\" data-index={i}>\n              {node}\n            <\/div>\n            {hasSeparator && (\n              <div\n                className=\"fancy-marquee__separator\"\n                data-react-fancy-marquee-separator=\"\"\n                aria-hidden=\"true\"\n              >\n                {separator}\n              <\/div>\n            )}\n          <\/Fragment>\n        ))}\n      <\/div>\n    );\n\n    \/\/ Two identical groups + translateX(-50%) keyframes = seamless wrap. Only\n    \/\/ the very first unit is exposed to AT; every repeat is aria-hidden.\n    const renderGroup = (duplicate: boolean) => (\n      <div\n        className=\"fancy-marquee__group\"\n        data-react-fancy-marquee-group=\"\"\n        aria-hidden={duplicate || undefined}\n      >\n        {Array.from({ length: copies }, (_, c) =>\n          <Fragment key={c}>\n            {renderUnit(duplicate || c > 0, !duplicate && c === 0 ? unitRef : undefined)}\n          <\/Fragment>,\n        )}\n      <\/div>\n    );\n\n    const cssVars: CSSProperties = {\n      \"--fancy-marquee-duration\": `${loopSeconds}s`,\n      \"--fancy-marquee-gap\": toLength(gap),\n      ...(fade !== false && {\n        \"--fancy-marquee-fade\": fade === true ? \"48px\" : toLength(fade),\n      }),\n    } as CSSProperties;\n\n    \/\/ Tilted strips widen slightly (the classic 102% \/ -1% trick) so the\n    \/\/ rotation never exposes the page background at the edges.\n    const angleStyle: CSSProperties = angle\n      ? { transform: `rotate(${angle}deg)`, width: \"102%\", marginLeft: \"-1%\" }\n      : {};\n\n    return (\n      <div\n        ref={rootRef}\n        data-react-fancy-marquee=\"\"\n        data-direction={direction}\n        data-paused={paused ? \"\" : undefined}\n        aria-hidden={decorative || undefined}\n        className={cn(\n          \"fancy-marquee\",\n          fade !== false && \"fancy-marquee--fade\",\n          pauseOnHover && \"fancy-marquee--pause-hover\",\n          className,\n        )}\n        style={{ ...cssVars, ...angleStyle, ...style }}\n        {...rest}\n      >\n        <div className=\"fancy-marquee__track\" data-react-fancy-marquee-track=\"\">\n          {renderGroup(false)}\n          {renderGroup(true)}\n        <\/div>\n      <\/div>\n    );\n  },\n);\n\nMarquee.displayName = \"Marquee\";\n","type":"registry:ui","target":"components\/fancy\/marquee\/Marquee.tsx"},{"path":"components\/fancy\/marquee\/Marquee.types.ts","content":"import type { HTMLAttributes, ReactNode } from \"react\";\n\nexport type MarqueeDirection = \"left\" | \"right\";\n\nexport interface MarqueeProps extends HTMLAttributes<HTMLDivElement> {\n  \/** Item nodes \u2014 each child is one strip item (alternative to `items`) *\/\n  children?: ReactNode;\n  \/** Data-driven items (alternative to children) \u2014 strings or nodes *\/\n  items?: ReactNode[];\n  \/**\n   * Scroll speed in px\/s \u2014 keeps perceived speed constant regardless of\n   * content width. Ignored when `duration` is set. Default: 40\n   *\/\n  speed?: number;\n  \/** Explicit seconds per loop (overrides `speed`) *\/\n  duration?: number;\n  \/** Scroll direction. Default: \"left\" *\/\n  direction?: MarqueeDirection;\n  \/** Pause the animation while hovered. Default: false *\/\n  pauseOnHover?: boolean;\n  \/** Controlled pause \u2014 `true` freezes the strip. Default: false *\/\n  paused?: boolean;\n  \/** Gap between items (number = px). Default: 40 *\/\n  gap?: number | string;\n  \/**\n   * Masked fade at the strip edges: `true` (48px, default), `false` to\n   * disable, or a number\/CSS length for a custom fade width\n   *\/\n  fade?: boolean | number | string;\n  \/** Optional node rendered between items *\/\n  separator?: ReactNode;\n  \/**\n   * Tilt the whole strip in degrees (e.g. `-1` for the torn, off-axis band\n   * look). The strip widens slightly to stay edge-to-edge. Default: 0\n   *\/\n  angle?: number;\n  \/**\n   * Decorative strips are `aria-hidden` (default). Set `false` to expose the\n   * content to assistive tech \u2014 the duplicated loop copy stays hidden either\n   * way. Default: true\n   *\/\n  decorative?: boolean;\n  className?: string;\n}\n","type":"registry:ui","target":"components\/fancy\/marquee\/Marquee.types.ts"},{"path":"components\/fancy\/marquee\/index.ts","content":"export { Marquee } from \".\/Marquee\";\nexport type { MarqueeProps, MarqueeDirection } from \".\/Marquee.types\";\n","type":"registry:ui","target":"components\/fancy\/marquee\/index.ts"}]}