{"version":3,"file":"marquee.cjs","names":[],"sources":["../src/content/marquee/marquee.ts"],"sourcesContent":["import { define, getHost, html, onMounted, prop } from '@vielzeug/ore';\nimport { watch } from '@vielzeug/ripple';\n\nimport type { ThemeColor } from '../../types';\n\nimport '../icon/icon';\nimport '../../inputs/button/button';\nimport componentStyles from './marquee.css?inline';\n\nexport type MarqueeDirection = 'left' | 'right';\n\nexport type OreMarqueeProps = {\n  /** Theme color for the navigation controls. */\n  color?: ThemeColor;\n  /** Scroll direction. Defaults to `'left'`. */\n  direction?: MarqueeDirection;\n  /** Duration in seconds for one complete ticker cycle. Defaults to `20`. */\n  duration?: number;\n  /** Pause the ticker while it is hovered. Defaults to `false`. */\n  'pause-on-hover'?: boolean;\n  /** Show previous/next navigation controls. Defaults to `true`. */\n  'show-controls'?: boolean;\n};\n\n/**\n * A CSS-driven, continuously scrolling ticker.\n *\n * Direct child elements are duplicated internally for a seamless loop. Cloned\n * content is inert and hidden from assistive technology, so author interactive\n * content only in the original children.\n *\n * @element ore-marquee\n *\n * @attr {string} color - Theme color for the navigation controls.\n * @attr {number} duration - Duration in seconds for one ticker cycle (default: 20)\n * @attr {string} direction - 'left' (default) | 'right'\n * @attr {boolean} pause-on-hover - Pause while hovered (default: false)\n * @attr {boolean} show-controls - Show previous/next controls (default: true)\n *\n * @cssprop --marquee-gap - Gap between ticker items (default: var(--size-4))\n *\n * @part track - The animated ticker track.\n * @example\n * ```html\n * <ore-marquee duration=\"24\" pause-on-hover>\n *   <span>Free shipping</span>\n *   <span>New arrivals</span>\n * </ore-marquee>\n * ```\n */\nexport const MARQUEE_TAG = 'ore-marquee' as const;\n\ndefine<OreMarqueeProps>(MARQUEE_TAG, {\n  props: {\n    color: prop.string<ThemeColor>(),\n    direction: prop.string<MarqueeDirection>('left'),\n    duration: prop.number(20),\n    'pause-on-hover': prop.bool(false),\n    'show-controls': prop.bool(true),\n  },\n  setup(props) {\n    const el = getHost();\n    let activeIndex = 0;\n    let clones: HTMLElement[] = [];\n    let items: HTMLElement[] = [];\n    let layoutFrame: number | null = null;\n    let loopWidth = 0;\n    let resizeObserver: ResizeObserver | null = null;\n    let track: HTMLElement | null = null;\n\n    const seekTo = (index: number): void => {\n      if (!track || items.length === 0) return;\n\n      activeIndex = (index + items.length) % items.length;\n\n      const item = items[activeIndex];\n\n      if (!item || loopWidth === 0) return;\n\n      const duration = props.duration.value ?? 20;\n      const delay = -((item.offsetLeft / loopWidth) * duration);\n\n      track.style.setProperty('animation-delay', `${delay}s`);\n    };\n\n    watch(\n      props.duration,\n      (duration) => {\n        el.style.setProperty('--_marquee-duration', `${duration ?? 20}s`);\n      },\n      { immediate: true },\n    );\n\n    onMounted(() => {\n      track = el.shadowRoot?.querySelector<HTMLElement>('.track') ?? null;\n\n      const slot = el.shadowRoot?.querySelector<HTMLSlotElement>('slot') ?? null;\n\n      const removeClones = (): void => {\n        clones.forEach((clone) => {\n          clone.remove();\n        });\n        clones = [];\n      };\n\n      const appendCloneSet = (): void => {\n        clones.push(\n          ...items.map((item) => {\n            const clone = item.cloneNode(true) as HTMLElement;\n\n            clone.setAttribute('aria-hidden', 'true');\n            clone.setAttribute('data-marquee-clone', '');\n            clone.setAttribute('inert', '');\n            el.appendChild(clone);\n\n            return clone;\n          }),\n        );\n      };\n\n      const syncLoop = (): void => {\n        if (!track || items.length === 0) return;\n\n        const marqueeTrack = track;\n\n        if (layoutFrame !== null) cancelAnimationFrame(layoutFrame);\n\n        removeClones();\n        loopWidth = 0;\n        marqueeTrack.style.removeProperty('--_marquee-translate');\n        appendCloneSet();\n\n        layoutFrame = requestAnimationFrame(() => {\n          layoutFrame = null;\n\n          const firstClone = clones[0];\n          const firstItem = items[0];\n\n          if (!firstClone || !firstItem) return;\n\n          loopWidth = firstClone.offsetLeft - firstItem.offsetLeft;\n\n          if (loopWidth <= 0) return;\n\n          const requiredSets = Math.max(2, Math.ceil((el.clientWidth + loopWidth) / loopWidth));\n\n          for (let set = 2; set < requiredSets; set += 1) appendCloneSet();\n\n          marqueeTrack.style.setProperty('--_marquee-translate', `-${loopWidth}px`);\n        });\n      };\n\n      const refreshItems = (): void => {\n        const nextItems = (slot?.assignedElements() ?? []).filter(\n          (child): child is HTMLElement => child instanceof HTMLElement && !child.hasAttribute('data-marquee-clone'),\n        );\n        const itemsChanged =\n          nextItems.length !== items.length || nextItems.some((item, index) => item !== items[index]);\n\n        if (!itemsChanged) return;\n\n        items = nextItems;\n        activeIndex = Math.min(activeIndex, Math.max(items.length - 1, 0));\n        syncLoop();\n      };\n\n      resizeObserver = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(syncLoop);\n      resizeObserver?.observe(el);\n      slot?.addEventListener('slotchange', refreshItems);\n      refreshItems();\n\n      return () => {\n        if (layoutFrame !== null) cancelAnimationFrame(layoutFrame);\n\n        resizeObserver?.disconnect();\n        slot?.removeEventListener('slotchange', refreshItems);\n        removeClones();\n        track = null;\n        items = [];\n        loopWidth = 0;\n      };\n    });\n\n    return html`\n      <div class=\"track\" part=\"track\" aria-live=\"off\">\n        <slot></slot>\n      </div>\n      ${() =>\n        props['show-controls'].value !== false\n          ? html`\n              <div class=\"controls\" part=\"controls\">\n                <ore-button\n                  class=\"nav-btn prev-btn\"\n                  part=\"prev-btn\"\n                  variant=\"ghost\"\n                  color=${() => props.color.value}\n                  rounded\n                  icon-only\n                  label=\"Previous item\"\n                  @click=${(event: Event) => {\n                    event.stopPropagation();\n                    seekTo(activeIndex - 1);\n                  }}>\n                  <ore-icon name=\"chevron-left\" size=\"16\" stroke-width=\"2\" aria-hidden=\"true\"></ore-icon>\n                </ore-button>\n                <ore-button\n                  class=\"nav-btn next-btn\"\n                  part=\"next-btn\"\n                  variant=\"ghost\"\n                  color=${() => props.color.value}\n                  rounded\n                  icon-only\n                  label=\"Next item\"\n                  @click=${(event: Event) => {\n                    event.stopPropagation();\n                    seekTo(activeIndex + 1);\n                  }}>\n                  <ore-icon name=\"chevron-right\" size=\"16\" stroke-width=\"2\" aria-hidden=\"true\"></ore-icon>\n                </ore-button>\n              </div>\n            `\n          : null}\n    `;\n  },\n  styles: [componentStyles],\n});\n"],"mappings":"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"}