{"version":3,"file":"TextFlip.V26CICKj.mjs","names":["useRef"],"sources":["https:/framerusercontent.com/modules/xZstCmMJ26sjZeKJpyQB/ECX15ugkCSBVOk4b24w7/TextFlip.js"],"sourcesContent":["import{jsx as _jsx}from\"react/jsx-runtime\";import{forwardRef,useEffect,useRef}from\"react\";import{RenderTarget}from\"framer\";const FLIP_ATTR=\"data-flip-unit\";const FLIP_HOST_ATTR=\"data-flip-host\";const FLIP_ACTIVE_ATTR=\"data-flip-active\";const FLIP_STYLE_ID=\"framer-text-flip-style\";const FLIP_EASING=\"cubic-bezier(0.22, 1, 0.36, 1)\";// Text inside these is not label text, and wrapping it in spans either breaks\n// the element or is silently discarded.\nconst FLIP_SKIP_SELECTOR=\"svg, script, style, noscript, textarea, input, select, option, [contenteditable='true'], [contenteditable='']\";function assignRef(ref,value){if(!ref)return;if(typeof ref===\"function\")ref(value);else ref.current=value;}function callAll(...callbacks){return event=>callbacks.forEach(callback=>callback?.(event));}function readLabel(node){if(!node)return\"\";return(node.getAttribute(\"aria-label\")||node.getAttribute(\"data-aria-label\")||\"\").trim();}/**\n * The group a layer belongs to, taken from the nearest accessibility label at\n * or above it.\n *\n * Reading upwards means the label can sit on the layer itself or on any frame\n * around it, which is what lets a trigger and the text it drives be labelled\n * once, at whatever level is convenient in the layer tree.\n */function resolveGroupLabel(node){let current=node;while(current){const label=readLabel(current);if(label)return label;current=current.parentElement;}return\"\";}/**\n * Flips, or releases, every text layer carrying the same label.\n *\n * Resolved by querying the document at the moment of the hover rather than by\n * keeping a registry: Framer writes `aria-label` onto the DOM after mount, so\n * anything captured when the override first ran would have recorded an empty\n * label and never matched.\n */function setGroupActive(label,active){if(!label||typeof document===\"undefined\")return;document.querySelectorAll(`[${FLIP_HOST_ATTR}]`).forEach(host=>{if(resolveGroupLabel(host)!==label)return;if(active)host.setAttribute(FLIP_ACTIVE_ATTR,\"true\");else host.removeAttribute(FLIP_ACTIVE_ATTR);});}/**\n * The hover rule, injected once per document.\n *\n * Hover has to be expressed in CSS rather than driven from script, because the\n * layer underneath is free to re-render on hover — a button that tracks its own\n * hover state in React does exactly that. Any re-render throws away the flip\n * units, so units moved by script snap back to rest at the very moment the\n * pointer arrives. A stylesheet rule applies to whatever units exist right now,\n * rebuilt or not.\n */function ensureFlipStyle(){if(document.getElementById(FLIP_STYLE_ID))return;const style=document.createElement(\"style\");style.id=FLIP_STYLE_ID;style.textContent=`\n[${FLIP_HOST_ATTR}]:hover [${FLIP_ATTR}] > *,\n[${FLIP_HOST_ATTR}]:focus-within [${FLIP_ATTR}] > *,\n[${FLIP_HOST_ATTR}][${FLIP_ACTIVE_ATTR}] [${FLIP_ATTR}] > * {\n    translate: 0 -100%;\n}`;document.head.appendChild(style);}/**\n * Every text node in the subtree that still needs splitting.\n *\n * Walks text nodes rather than elements, so the override can be applied to a\n * wrapper — a frame, a button, a whole component — and still reach the text\n * however deeply it is nested. That also covers formatted text, where a line\n * starting with a bold run is a `<strong>` followed by loose text inside the\n * same paragraph: looking for \"leaf elements with text\" finds the `<strong>`\n * but never the loose text.\n */function collectTextNodes(host){const walker=document.createTreeWalker(host,NodeFilter.SHOW_TEXT,{acceptNode(node){if(!node.textContent||!node.textContent.trim()){return NodeFilter.FILTER_REJECT;}const parent=node.parentElement;if(!parent)return NodeFilter.FILTER_REJECT;if(parent.closest(`[${FLIP_ATTR}]`))return NodeFilter.FILTER_REJECT;if(parent.closest(FLIP_SKIP_SELECTOR)){return NodeFilter.FILTER_REJECT;}return NodeFilter.FILTER_ACCEPT;}});const nodes=[];let current=walker.nextNode();while(current){nodes.push(current);current=walker.nextNode();}return nodes;}/**\n * One flippable unit: the visible copy, plus a second copy parked directly\n * below it, inside a box that clips both.\n *\n * On hover the pair slides up by its own height, so the first copy leaves\n * through the top as the second arrives from the bottom — one roll, no gap.\n *\n * The clone is `aria-hidden` and the original stays real text, so the layer is\n * still selectable and still reads correctly to a screen reader.\n */function createFlipUnit(text,index,durationMs,staggerMs){const unit=document.createElement(\"span\");unit.setAttribute(FLIP_ATTR,\"true\");unit.style.display=\"inline-block\";unit.style.position=\"relative\";unit.style.overflow=\"hidden\";// Without this the clipping box sits on the text baseline and shaves the\n// descenders off letters like g and y.\nunit.style.verticalAlign=\"top\";unit.style.whiteSpace=\"pre\";const transition=`translate ${durationMs}ms ${FLIP_EASING} ${index*staggerMs}ms`;const front=document.createElement(\"span\");front.style.display=\"block\";front.style.transition=transition;front.textContent=text;const back=document.createElement(\"span\");back.setAttribute(\"aria-hidden\",\"true\");back.style.display=\"block\";back.style.position=\"absolute\";back.style.left=\"0\";back.style.top=\"100%\";back.style.transition=transition;back.textContent=text;unit.appendChild(front);unit.appendChild(back);return unit;}/**\n * Wraps each character or word in a flip unit, in place.\n *\n * Each unit stays inside the element that held its text, so a bold run keeps\n * its `<strong>` and a link keeps its `<a>`. Whitespace is left as plain text\n * nodes between the units, so the line still wraps the way Framer laid it out.\n */function splitIntoUnits(host,byWord,durationMs,staggerMs){let index=0;collectTextNodes(host).forEach(textNode=>{const source=textNode.textContent??\"\";const fragment=document.createDocumentFragment();const chunks=byWord?source.split(/(\\s+)/):Array.from(source);chunks.forEach(chunk=>{if(!chunk)return;if(!chunk.trim()){fragment.appendChild(document.createTextNode(chunk));return;}fragment.appendChild(createFlipUnit(chunk,index,durationMs,staggerMs));index+=1;});textNode.replaceWith(fragment);});return index;}function getFlipUnits(host){return Array.from(host.querySelectorAll(`[${FLIP_ATTR}]`));}/**\n * Unwraps the units and merges the text back together, which restores the\n * original node structure without having to remember what it was.\n */function restoreUnits(host){getFlipUnits(host).forEach(unit=>{const text=unit.firstElementChild?.textContent??\"\";unit.replaceWith(document.createTextNode(text));});host.normalize();}/**\n * Prepares one element and returns its teardown.\n *\n * Kept imperative rather than living inside an effect, because the node can\n * arrive from two different places — see `useFlipHost`.\n */function attachFlip(host,byWord,durationMs,staggerMs){ensureFlipStyle();host.setAttribute(FLIP_HOST_ATTR,\"true\");let isSplitting=false;const ensureSplit=()=>{isSplitting=true;splitIntoUnits(host,byWord,durationMs,staggerMs);window.requestAnimationFrame(()=>{isSplitting=false;});};ensureSplit();// Framer owns this element's children: any re-render drops the flip units,\n// so they are rebuilt as soon as that happens.\nconst observer=new MutationObserver(()=>{if(isSplitting)return;ensureSplit();});observer.observe(host,{childList:true,subtree:true,characterData:true});return()=>{observer.disconnect();host.removeAttribute(FLIP_HOST_ATTR);host.removeAttribute(FLIP_ACTIVE_ATTR);restoreUnits(host);};}/**\n * Resolves the element to work on, from either of the two ways it can arrive.\n *\n * A forwarded ref is the normal route, but it does not always land. React 18\n * intercepts `ref` in JSX, so a plain function component — one that expects to\n * read `props.ref` itself instead of being wrapped in `forwardRef` — never\n * receives it, and the Override ends up holding nothing. Those components do\n * still forward their pointer handlers, so the first pointer event carries the\n * element in `currentTarget` and the setup runs from there instead.\n */function useFlipHost(enabled,setup){const nodeRef=useRef(null);const cleanupRef=useRef(null);const adopt=node=>{if(!enabled||node===nodeRef.current)return;cleanupRef.current?.();cleanupRef.current=null;nodeRef.current=node;if(node)cleanupRef.current=setup(node);};useEffect(()=>{return()=>{cleanupRef.current?.();cleanupRef.current=null;nodeRef.current=null;};},[]);return{nodeRef,adopt};}function createTextFlip(byWord,durationMs,staggerMs){return function withFlip(Component){return /*#__PURE__*/forwardRef(function TextFlipOverride(props,ref){const isCanvas=RenderTarget.current()===RenderTarget.canvas;// Leaving the text untouched is the resting state, so reduced\n// motion needs no separate rendering path.\nconst enabled=!isCanvas&&typeof window!==\"undefined\"&&!window.matchMedia?.(\"(prefers-reduced-motion: reduce)\")?.matches;const{nodeRef,adopt}=useFlipHost(enabled,node=>attachFlip(node,byWord,durationMs,staggerMs));const adoptFromEvent=event=>{if(nodeRef.current)return;const node=event?.currentTarget;if(node instanceof HTMLElement)adopt(node);};return /*#__PURE__*/_jsx(Component,{...props,ref:node=>{adopt(node instanceof HTMLElement?node:null);assignRef(ref,node);},onMouseEnter:callAll(props.onMouseEnter,adoptFromEvent),onFocus:callAll(props.onFocus,adoptFromEvent)});});};}/**\n * Drives a text-flip layer from somewhere else on the page.\n *\n * Pairing is by accessibility label: give the trigger and the text the same\n * Label in Framer, and hovering the trigger flips the text. The label may sit\n * on either layer itself or on a frame around it, and one trigger drives every\n * text layer that shares its label.\n *\n * Driven from the pointer handlers rather than from a ref, so it also works on\n * a code component that never forwards its ref but does chain its handlers.\n */function createTextFlipTrigger(){return function withTrigger(Component){return /*#__PURE__*/forwardRef(function TextFlipTriggerOverride(props,ref){const isCanvas=RenderTarget.current()===RenderTarget.canvas;const activeRef=useRef(\"\");const activate=event=>{if(isCanvas)return;const node=event?.currentTarget;// Resolved per event, not once on mount: Framer writes\n// `aria-label` to the DOM after mount, and it can be edited\n// while a Preview is open.\nconst label=(node instanceof HTMLElement?readLabel(node)||resolveGroupLabel(node):\"\")||String(props[\"aria-label\"]||props.ariaLabel||\"\").trim();if(!label){console.warn(\"[TextFlip] This trigger has no accessibility label, so it cannot be matched to any text. Give the trigger and the text the same Label in Framer.\");return;}activeRef.current=label;setGroupActive(label,true);};const release=()=>{if(!activeRef.current)return;setGroupActive(activeRef.current,false);activeRef.current=\"\";};useEffect(()=>release,[]);return /*#__PURE__*/_jsx(Component,{...props,ref:ref,onMouseEnter:callAll(props.onMouseEnter,activate),onMouseLeave:callAll(props.onMouseLeave,release),onFocus:callAll(props.onFocus,activate),onBlur:callAll(props.onBlur,release)});});};}/**\n * Framer hover text-flip overrides.\n *\n * On hover each letter rolls up and is replaced by a copy of itself arriving\n * from below, one after the other. Colour, font and size stay with the layer —\n * these overrides never touch them.\n *\n * Usage:\n * - Apply it to the text layer, or to anything containing text: a frame, a\n *   button, a whole component. Every text node inside is flipped, and hovering\n *   anywhere in that layer triggers all of them together.\n * - To drive the flip from elsewhere, put `withTextFlipTrigger` on the other\n *   layer and give both the same Label in Framer.\n * - The layer needs room for the roll: the effect clips to the line box, so a\n *   layer with its height set tight to the text will cut the motion off.\n * - Works on layers that re-render while hovered, such as a button tracking its\n *   own hover state: the flip is driven by a CSS rule, not by script.\n * - On Canvas the text renders untouched; the flip runs in Preview and on the\n *   published site.\n * - Readers with reduced motion enabled see plain, static text.\n */export function withTextFlip(Component){return createTextFlip(false,400,18)(Component);}/**\n * Same roll, but every letter moves together — no cascade.\n */export function withTextFlipInstant(Component){return createTextFlip(false,320,0)(Component);}/**\n * Flips whole words instead of letters. Steadier on long text, where a\n * per-letter cascade turns into a long ripple.\n */export function withTextFlipWords(Component){return createTextFlip(true,420,45)(Component);}/**\n * The text side of a trigger pair. Identical to `withTextFlip`, named for the\n * pairing — any of the overrides above works as content just as well.\n */export function withTextFlipContent(Component){return createTextFlip(false,400,18)(Component);}/**\n * Word-by-word content, for longer text.\n */export function withTextFlipContentWords(Component){return createTextFlip(true,420,45)(Component);}/**\n * Apply to the layer that should drive the flip — a button, a card, a row.\n */export function withTextFlipTrigger(Component){return createTextFlipTrigger()(Component);}\nexport const __FramerMetadata__ = {\"exports\":{\"withTextFlipContent\":{\"type\":\"reactHoc\",\"name\":\"withTextFlipContent\",\"annotations\":{\"framerContractVersion\":\"1\"}},\"withTextFlip\":{\"type\":\"reactHoc\",\"name\":\"withTextFlip\",\"annotations\":{\"framerContractVersion\":\"1\"}},\"withTextFlipTrigger\":{\"type\":\"reactHoc\",\"name\":\"withTextFlipTrigger\",\"annotations\":{\"framerContractVersion\":\"1\"}},\"withTextFlipWords\":{\"type\":\"reactHoc\",\"name\":\"withTextFlipWords\",\"annotations\":{\"framerContractVersion\":\"1\"}},\"withTextFlipInstant\":{\"type\":\"reactHoc\",\"name\":\"withTextFlipInstant\",\"annotations\":{\"framerContractVersion\":\"1\"}},\"withTextFlipContentWords\":{\"type\":\"reactHoc\",\"name\":\"withTextFlipContentWords\",\"annotations\":{\"framerContractVersion\":\"1\"}},\"__FramerMetadata__\":{\"type\":\"variable\"}}}\n//# sourceMappingURL=./TextFlip.map"],"mappings":"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"}