> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mochacola.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# React

> Kenny's React playground — small interactive projects for learning Mintlify

export const CopySnippet = () => {
  const [copied, setCopied] = useState(false);
  const code = "npm install @mintlify/cli";
  const handleCopy = () => {
    navigator.clipboard.writeText(code).then(() => {
      setCopied(true);
      setTimeout(() => setCopied(false), 1800);
    });
  };
  return <div className="copy-snippet not-prose">
      <code className="copy-snippet-code">{code}</code>
      <button type="button" className={"copy-snippet-btn" + (copied ? " copied" : "")} onClick={handleCopy} aria-label={copied ? "Copied to clipboard" : "Copy to clipboard"}>
        {copied ? <svg viewBox="0 0 24 24" className="copy-snippet-icon pop">
            <path className="copy-snippet-check" d="M5 13l4 4L19 7" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
          </svg> : <svg viewBox="0 0 24 24" className="copy-snippet-icon">
            <rect x="9" y="9" width="11" height="11" rx="2" fill="none" stroke="currentColor" strokeWidth="2" />
            <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
          </svg>}
        {copied && <span className="copy-snippet-tooltip">Copied!</span>}
      </button>
    </div>;
};

export const StatCounter = ({value, suffix = "", label, duration = 1400}) => {
  const [display, setDisplay] = useState(0);
  const ref = useRef(null);
  const started = useRef(false);
  useEffect(() => {
    const node = ref.current;
    if (!node) return;
    const observer = new IntersectionObserver(entries => {
      entries.forEach(entry => {
        if (entry.isIntersecting && !started.current) {
          started.current = true;
          const start = performance.now();
          const tick = now => {
            const progress = Math.min((now - start) / duration, 1);
            const eased = 1 - Math.pow(1 - progress, 3);
            setDisplay(Math.round(eased * value));
            if (progress < 1) requestAnimationFrame(tick);
          };
          requestAnimationFrame(tick);
        }
      });
    }, {
      threshold: 0.3
    });
    observer.observe(node);
    return () => observer.disconnect();
  }, [value, duration]);
  return <div className="stat-counter" ref={ref}>
      <span className="stat-counter-value">
        {display}
        {suffix}
      </span>
      <span className="stat-counter-label">{label}</span>
    </div>;
};

export const StatCounterGroup = () => {
  return <div className="stat-counter-group not-prose">
      <StatCounter value={500} suffix="+" label="Cups of coffee" />
      <StatCounter value={42} label="Docs pages shipped" />
      <StatCounter value={99} suffix="%" label="Uptime" />
    </div>;
};

export const FeedbackWidget = () => {
  const [feedback, setFeedback] = useState(null);
  return <div className="feedback-widget not-prose">
      {feedback === null ? <div className="feedback-prompt">
          <span className="feedback-question">Was this page helpful?</span>
          <div className="feedback-buttons">
            <button type="button" className="feedback-btn" aria-label="Yes, this page was helpful" onClick={() => setFeedback("yes")}>
              👍
            </button>
            <button type="button" className="feedback-btn" aria-label="No, this page was not helpful" onClick={() => setFeedback("no")}>
              👎
            </button>
          </div>
        </div> : <div className="feedback-confirm" role="status">
          <svg className="feedback-check" viewBox="0 0 52 52">
            <circle className="feedback-check-circle" cx="26" cy="26" r="24" />
            <path className="feedback-check-mark" d="M14 27l7 7 17-17" />
          </svg>
          <span>{feedback === "yes" ? "Thanks for the feedback!" : "Thanks — we'll work on it."}</span>
        </div>}
    </div>;
};

<Accordion title="Docs feedback widget" defaultOpen={false}>
  <Accordion title="How this was built" defaultOpen={false}>
    This is a real React component, declared inline right in the MDX file with `export const FeedbackWidget = () => {...}`. Mintlify pre-injects hooks like `useState` into MDX — no `import` needed, and no separate `/snippets/` file required for a component this small.

    Clicking a thumb sets `feedback` to `"yes"` or `"no"`, which swaps the rendered JSX from the prompt to a confirmation view. The checkmark is an inline SVG whose circle and check mark draw themselves in with `stroke-dasharray` / `stroke-dashoffset` animations, defined in `custom.css`.
  </Accordion>

  <FeedbackWidget />
</Accordion>

<Accordion title="Copy to clipboard button" defaultOpen={false}>
  <Accordion title="How this was built" defaultOpen={false}>
    Another inline component, `CopySnippet`, using `useState` for a `copied` boolean. Clicking the button calls `navigator.clipboard.writeText(code)`; once that promise resolves, `copied` flips to `true`, which swaps the button's icon from a copy glyph to a checkmark and shows a floating "Copied!" tooltip. A `setTimeout` flips it back after 1.8 seconds.

    The checkmark path animates in with the same `stroke-dasharray` / `stroke-dashoffset` trick as the feedback widget's checkmark, plus a small spring `scale()` pop on the icon itself — both defined in `custom.css`.
  </Accordion>

  <CopySnippet />
</Accordion>

<Accordion title="Animated stat counter" defaultOpen={false}>
  <Accordion title="How this was built" defaultOpen={false}>
    `StatCounter` uses `useRef` to grab its own DOM node, then a `useEffect` sets up an `IntersectionObserver` watching that node. The first time it becomes at least 30% visible, a `requestAnimationFrame` loop runs for `duration` milliseconds, easing the displayed number from `0` up to `value` with an ease-out-cubic curve (`1 - (1 - progress) ** 3`) and writing each frame to state with `setDisplay`. A `started` ref guards against re-triggering if the counter scrolls in and out of view again.

    Using `IntersectionObserver` instead of a raw `scroll` listener means it fires correctly even when the counter starts hidden inside a collapsed `Accordion` — the count-up plays the moment you open this section, same as it would scrolling down a normal page.
  </Accordion>

  <StatCounterGroup />
</Accordion>
