// components.jsx — shared primitives: BlurText, FadeUp, Icons, CTAs, VideoCard, StickyCTA, Tweaks
const { useState, useEffect, useRef, useCallback, useMemo } = React;
// ---------- Icons (inline SVG, minimal) ----------
const Icon = {
ArrowUpRight: ({ size = 16 }) => (
),
Play: ({ size = 22 }) => (
),
ChevronDown: ({ size = 18 }) => (
),
Shield: ({ size = 22 }) => (
),
Lock: ({ size = 22 }) => (
),
Bolt: ({ size = 22 }) => (
),
MapPin: ({ size = 18 }) => (
),
Star: ({ size = 16 }) => (
),
Scales: ({ size = 22 }) => (
),
X: ({ size = 14 }) => (
),
Instagram: ({ size = 14 }) => (
),
TikTok: ({ size = 14 }) => (
),
};
// ---------- BlurText ----------
function BlurText({ children, className = "", delay = 0, stagger = 90, as: As = "span" }) {
const ref = useRef(null);
const [inView, setInView] = useState(true);
useEffect(() => {
const el = ref.current; if (!el) return;
const r = el.getBoundingClientRect();
if (r.top < window.innerHeight && r.bottom > 0) { setInView(true); return; }
setInView(false);
const io = new IntersectionObserver(([e]) => { if (e.isIntersecting) { setInView(true); io.disconnect(); } }, { threshold: 0.05 });
io.observe(el); return () => io.disconnect();
}, []);
let wordIdx = 0;
const renderString = (s, keyPrefix) => {
const tokens = s.split(/(\s+)/);
return tokens.map((tok, pi) => {
if (tok === "") return null;
if (/^\s+$/.test(tok)) return {"\u00A0"};
const i = wordIdx++;
const d = delay + i * stagger;
return (
{tok}
);
});
};
const arr = React.Children.toArray(children);
const out = arr.map((c, ci) => {
if (typeof c === "string") return {renderString(c, `c${ci}`)};
if (React.isValidElement(c) && c.type === "br") return
;
if (React.isValidElement(c)) {
const inner = typeof c.props.children === "string" ? renderString(c.props.children, `e${ci}`) : c.props.children;
return React.cloneElement(c, { key: `e${ci}` }, inner);
}
return {c};
});
return {out};
}
// ---------- FadeUp ----------
function FadeUp({ children, delay = 0, className = "", as: As = "div", style = {} }) {
const ref = useRef(null);
const [inView, setInView] = useState(false);
useEffect(() => {
const el = ref.current; if (!el) return;
const r = el.getBoundingClientRect();
if (r.top < window.innerHeight && r.bottom > 0) { setInView(true); return; }
const io = new IntersectionObserver(([e]) => { if (e.isIntersecting) { setInView(true); io.disconnect(); } }, { threshold: 0.05 });
io.observe(el); return () => io.disconnect();
}, []);
return (
{children}
);
}
// ---------- CTA ----------
function CTA({ href, children, variant = "primary", onClick, className = "" }) {
const cls = variant === "primary" ? "cta cta-primary liquid-glass-strong"
: variant === "red" ? "cta-red-fill"
: "cta-ghost";
const Comp = href ? "a" : "button";
return (
{children}
{variant === "ghost"
?
: }
);
}
// ---------- Kicker ----------
function Kicker({ n, children }) {
return
§{n} — {children}
;
}
// ---------- VideoBg (hydration-safe background video) ----------
function VideoBg({ src, poster, style }) {
// Smoke video replaced with lightweight CSS blob background (2026-04-23).
// Signature preserved so existing call sites don't need changes.
return (
);
}
// ---------- VideoCard (PBAL clips) ----------
function VideoCard({ poster, mp4, hls, caption }) {
const [playing, setPlaying] = useState(false);
const vref = useRef(null);
const play = () => {
const v = vref.current; if (!v) return;
if (!v.src && !v._hlsAttached) {
if (window.Hls && window.Hls.isSupported() && hls) {
try {
const hlsjs = new window.Hls();
hlsjs.loadSource(hls);
hlsjs.attachMedia(v);
v._hlsAttached = true;
} catch (e) {
v.src = mp4;
}
} else if (hls && v.canPlayType && v.canPlayType("application/vnd.apple.mpegurl")) {
v.src = hls;
} else {
v.src = mp4;
}
}
v.play().catch(() => {});
setPlaying(true);
};
return (
{!playing &&

}
{!playing && (
<>
{caption}
>
)}
);
}
// ---------- Sticky CTA ----------
function StickyCTA() {
const [visible, setVisible] = useState(false);
useEffect(() => {
const onScroll = () => setVisible(window.scrollY > window.innerHeight * 0.85);
window.addEventListener("scroll", onScroll, { passive: true });
onScroll();
return () => window.removeEventListener("scroll", onScroll);
}, []);
return (
Shop Smell-Proof Bags
);
}
// ---------- Tweaks panel ----------
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
"heroImage": "mini_duffle",
"accent": "pbal_red",
"theme": "dark",
"headlineVariant": "A"
}/*EDITMODE-END*/;
function TweaksPanel({ state, setState }) {
const [editMode, setEditMode] = useState(false);
useEffect(() => {
const onMsg = (e) => {
const d = e.data || {};
if (d.type === "__activate_edit_mode") setEditMode(true);
if (d.type === "__deactivate_edit_mode") setEditMode(false);
};
window.addEventListener("message", onMsg);
window.parent.postMessage({ type: "__edit_mode_available" }, "*");
return () => window.removeEventListener("message", onMsg);
}, []);
const update = (k, v) => {
setState(s => ({ ...s, [k]: v }));
window.parent.postMessage({ type: "__edit_mode_set_keys", edits: { [k]: v } }, "*");
};
if (!editMode) return null;
const Opt = ({ k, v, label }) => (
);
return (
);
}
// Export to window
Object.assign(window, { Icon, BlurText, FadeUp, CTA, Kicker, VideoBg, VideoCard, StickyCTA, TweaksPanel, TWEAK_DEFAULTS });