{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "prose",
  "type": "registry:ui",
  "title": "Prose",
  "description": "Token-only typography for rendered rich text, with section links and code copy.",
  "categories": [
    "content"
  ],
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://www.stealth.pm/r/tokens.json",
    "https://www.stealth.pm/r/cn.json",
    "https://www.stealth.pm/r/icons.json",
    "https://www.stealth.pm/r/motion.json",
    "https://www.stealth.pm/r/copy-button.json"
  ],
  "files": [
    {
      "path": "components/ui/prose.tsx",
      "content": "\"use client\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { CopyButton, useCopy } from \"@/components/ui/copy-button\";\nimport { cn } from \"@/lib/cn\";\nimport { ease } from \"@/lib/motion\";\nimport { Check, Link as LinkIcon } from \"@/lib/icons\";\n\n/* -------------------------------------------------------------------------------------------------\n * Type\n *\n * Every rule sits inside :where(), so it has no specificity: a class on any element inside\n * wins without a fight. Sizes are three sets of custom properties, so the rhythm (flow space,\n * heading steps, code size) scales as one system instead of being re-declared per size.\n * -----------------------------------------------------------------------------------------------*/\n\nconst ARROW =\n  \"url(\\\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='currentColor' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 11 11 5M6 5h5v5'/%3E%3C/svg%3E\\\")\";\nconst CHEVRON =\n  \"url(\\\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='currentColor' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6.25 4.5 3.5 3.5-3.5 3.5'/%3E%3C/svg%3E\\\")\";\nconst TICK =\n  \"url(\\\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.5 8.5 6.5 11.5 12.5 4.5'/%3E%3C/svg%3E\\\")\";\n\nconst CSS = `\n[data-prose]{--p-size:15px;--p-lh:1.65;--p-h1:28px;--p-h2:20px;--p-h3:16.5px;--p-h4:15px;--p-code:12.5px;--p-flow:1.05em;\n  --sh-keyword:var(--fg);--sh-class:var(--fg);--sh-entity:var(--fg);--sh-identifier:var(--fg);--sh-property:var(--fg-2);--sh-string:var(--fg-2);--sh-jsxliterals:var(--fg-2);--sh-sign:var(--fg-3);--sh-comment:var(--fg-4);\n  position:relative;font-size:var(--p-size);line-height:var(--p-lh);color:var(--fg-2);overflow-wrap:break-word}\n[data-prose][data-size=sm]{--p-size:13.5px;--p-lh:1.6;--p-h1:20px;--p-h2:16px;--p-h3:14px;--p-h4:13.5px;--p-code:12px;--p-flow:.9em}\n[data-prose][data-size=lg]{--p-size:17px;--p-lh:1.7;--p-h1:34px;--p-h2:24px;--p-h3:19px;--p-h4:17px;--p-code:13.5px;--p-flow:1.15em}\n[data-prose][data-measure]{max-width:68ch}\n\n:where([data-prose-body]) :where(h1,h2,h3,h4,h5,h6,p,ul,ol,dl,pre,blockquote,table,figure,hr,details,img,video){margin:var(--p-flow) 0 0}\n:where([data-prose-body]) > :first-child,:where([data-prose-body]) :where(li,blockquote,dd,figure,details) > :first-child{margin-top:0}\n\n:where([data-prose-body]) :where(h1,h2,h3,h4,h5,h6){color:var(--fg);font-weight:500;text-wrap:balance;scroll-margin-top:5rem}\n:where([data-prose-body]) :where(h1){font-size:var(--p-h1);line-height:1.15;letter-spacing:-.025em;font-weight:600}\n:where([data-prose-body]) :where(h2){font-size:var(--p-h2);line-height:1.3;letter-spacing:-.02em;margin-top:1.9em}\n:where([data-prose-body]) :where(h3){font-size:var(--p-h3);line-height:1.4;letter-spacing:-.012em;margin-top:1.8em}\n:where([data-prose-body]) :where(h4,h5,h6){font-size:var(--p-h4);line-height:1.45;letter-spacing:-.005em;margin-top:1.6em}\n:where([data-prose-body]) :where(h1,h2,h3,h4,h5,h6) + *{margin-top:.55em}\n:where([data-prose-body]) :where(h1) + *{margin-top:.8em}\n:where([data-prose-body]) :where(p,li,dd,blockquote,figcaption){text-wrap:pretty}\n:where([data-prose-body]) :where(strong,b){color:var(--fg);font-weight:500}\n:where([data-prose-body]) :where(small){font-size:.86em}\n:where([data-prose-body]) :where(mark){color:var(--fg);background:color-mix(in oklab,var(--fg) 12%,transparent);border-radius:3px;padding:0 .15em;box-decoration-break:clone;-webkit-box-decoration-break:clone}\n:where([data-prose-body]) :where(abbr[title]){text-decoration:underline dotted var(--fg-4);text-underline-offset:3px;cursor:help}\n:where([data-prose-body]) :where(sub,sup){font-size:.75em;line-height:0}\n:where([data-prose-body]) :where(sup a){font-family:var(--font-mono);text-decoration:none;padding:0 .1em}\n\n:where([data-prose-body]) :where(a){color:var(--fg);text-decoration:underline;text-decoration-color:var(--fg-4);text-decoration-thickness:1px;text-underline-offset:3px;border-radius:3px;transition:text-decoration-color .16s ease-out}\n:where([data-prose-body]) :where(a):hover{text-decoration-color:var(--fg-2)}\n:where([data-prose-body]) :where(a):focus-visible{outline:1px solid var(--fg-3);outline-offset:2px}\n:where([data-prose-body]) :where(a[target=_blank])::after{content:\"\";display:inline-block;width:.72em;height:.72em;margin-left:.12em;vertical-align:.02em;background:currentColor;opacity:.45;-webkit-mask:${ARROW} center/contain no-repeat;mask:${ARROW} center/contain no-repeat;transition:translate .2s var(--ease-out-expo),opacity .16s ease-out}\n:where([data-prose-body]) :where(a[target=_blank]):hover::after{translate:1px -1px;opacity:.9}\n\n:where([data-prose-body]) :where(ul,ol){padding-left:1.35em}\n:where([data-prose-body]) :where(ul){list-style:disc}\n:where([data-prose-body]) :where(ol){list-style:decimal}\n:where([data-prose-body]) :where(ul ul){list-style:circle}\n:where([data-prose-body]) :where(li){margin-top:.3em;padding-left:.2em}\n:where([data-prose-body]) :where(li)::marker{color:var(--fg-4)}\n:where([data-prose-body]) :where(ol > li)::marker{font-variant-numeric:tabular-nums;color:var(--fg-3)}\n:where([data-prose-body]) :where(li > ul,li > ol){margin-top:.3em}\n:where([data-prose-body]) :where(li:has(> input[type=checkbox])){list-style:none;margin-left:-1.3em;padding-left:0}\n:where([data-prose-body]) :where(li > input[type=checkbox]){appearance:none;width:.9em;height:.9em;margin:0 .45em 0 0;vertical-align:-.1em;border:1px solid var(--line-2);border-radius:4px;background:var(--raised)}\n:where([data-prose-body]) :where(li > input[type=checkbox]:checked){border-color:var(--fg);background:var(--fg)}\n:where([data-prose-body]) :where(li > input[type=checkbox]:checked)::after{content:\"\";display:block;width:100%;height:100%;background:var(--frame);-webkit-mask:${TICK} center/75% no-repeat;mask:${TICK} center/75% no-repeat}\n:where([data-prose-body]) :where(dt){color:var(--fg);font-weight:500;margin-top:var(--p-flow)}\n:where([data-prose-body]) :where(dd){margin:.2em 0 0}\n\n:where([data-prose-body]) :where(blockquote){padding-left:1em;border-left:2px solid var(--line-2);color:var(--fg-2)}\n:where([data-prose-body]) :where(hr){border:0;border-top:1px solid var(--line);margin:2.2em 0}\n\n:where([data-prose-body]) :where(code,kbd,samp){font-family:var(--font-mono);font-size:.86em;font-variant-ligatures:none}\n:where([data-prose-body]) :where(:not(pre) > code){color:var(--fg);padding:.12em .36em;border:1px solid var(--line);border-radius:5px;background:var(--raised);overflow-wrap:anywhere;box-decoration-break:clone;-webkit-box-decoration-break:clone}\n:where([data-prose-body]) :where(kbd){display:inline-block;min-width:1.6em;padding:.05em .4em;text-align:center;color:var(--fg-2);border:1px solid var(--line-2);border-bottom-width:2px;border-radius:5px;background:var(--raised);line-height:1.4}\n:where([data-prose-body]) :where(pre){position:relative;overflow-x:auto;padding:.95em 1.1em;border:1px solid var(--line);border-radius:12px;background:var(--raised);color:var(--fg);font-family:var(--font-mono);font-size:var(--p-code);line-height:1.65;tab-size:2;scrollbar-width:thin;overscroll-behavior-x:contain}\n:where([data-prose-body]) :where(pre):focus-visible{outline:1px solid var(--fg-3);outline-offset:2px}\n:where([data-prose-body]) :where(pre code){font-size:inherit;color:inherit;background:none;border:0;padding:0;white-space:pre}\n:where([data-prose-body]) :where(pre[data-language])::before{content:attr(data-language);position:absolute;top:.55em;right:.8em;font-size:10.5px;letter-spacing:.08em;text-transform:uppercase;color:var(--fg-4);transition:opacity .12s ease-out;pointer-events:none}\n:where([data-prose-body]) :where(pre[data-prose-active])::before{opacity:0}\n\n:where([data-prose-body]) :where(table){display:block;overflow-x:auto;width:max-content;max-width:100%;border:1px solid var(--line);border-radius:10px;border-spacing:0;border-collapse:separate;font-size:.9em;line-height:1.5;scrollbar-width:thin}\n:where([data-prose-body]) :where(th,td){padding:.55em .9em;text-align:left;vertical-align:top;border-bottom:1px solid var(--line)}\n:where([data-prose-body]) :where(th){color:var(--fg);font-weight:500;background:var(--raised);white-space:nowrap}\n:where([data-prose-body]) :where(tr:last-child > td){border-bottom:0}\n:where([data-prose-body]) :where(th[align=right],td[align=right]){text-align:right;font-variant-numeric:tabular-nums}\n:where([data-prose-body]) :where(th[align=center],td[align=center]){text-align:center}\n:where([data-prose-body]) :where(td code){overflow-wrap:normal}\n\n:where([data-prose-body]) :where(img,video){display:block;max-width:100%;height:auto;border-radius:10px;outline:1px solid var(--line);outline-offset:-1px;transition:opacity .32s var(--ease-out-expo),scale .5s var(--ease-out-expo)}\n:where([data-prose-body][data-enhanced]) :where(img[data-state=loading]){opacity:0;scale:1.015}\n:where([data-prose-body]) :where(img[data-state=error]){min-height:6em;background:var(--raised);color:var(--fg-3);font-size:.86em}\n:where([data-prose-body]) :where(figure > img,figure > video){margin:0 auto}\n:where([data-prose-body]) :where(figure){margin-top:calc(var(--p-flow) * 1.6)}\n:where([data-prose-body]) :where(figure) + *{margin-top:calc(var(--p-flow) * 1.6)}\n:where([data-prose-body]) :where(figcaption){margin-top:.75em;font-size:.84em;line-height:1.5;color:var(--fg-3);text-align:center;text-wrap:balance}\n\n:where([data-prose-body]) :where(details){border:1px solid var(--line);border-radius:10px;padding:.55em .9em;interpolate-size:allow-keywords}\n:where([data-prose-body]) :where(summary){display:flex;align-items:center;gap:.45em;cursor:pointer;color:var(--fg);font-weight:500;list-style:none;border-radius:4px}\n:where([data-prose-body]) :where(summary)::-webkit-details-marker{display:none}\n:where([data-prose-body]) :where(summary)::before{content:\"\";flex:none;width:.9em;height:.9em;background:var(--fg-3);-webkit-mask:${CHEVRON} center/contain no-repeat;mask:${CHEVRON} center/contain no-repeat;transition:rotate .2s var(--ease-out-expo)}\n:where([data-prose-body]) :where(details[open] > summary)::before{rotate:90deg}\n:where([data-prose-body]) :where(summary):focus-visible{outline:1px solid var(--fg-3);outline-offset:2px}\n:where([data-prose-body]) :where(details)::details-content{block-size:0;overflow:clip;transition:block-size .24s var(--ease-in-out-quart),content-visibility .24s allow-discrete}\n:where([data-prose-body]) :where(details[open])::details-content{block-size:auto}\n:where([data-prose-body]) :where(details[open] > summary + *){margin-top:.6em}\n\n@media (prefers-reduced-motion: reduce){:where([data-prose-body][data-enhanced]) :where(img[data-state=loading]){scale:1}}\n`;\n\n/* -------------------------------------------------------------------------------------------------\n * Enhancements: things HTML can't say on its own, applied to whatever is rendered inside,\n * including content that streams in later. Only attributes are set, never structure, so\n * React-rendered children are never fought over.\n * -----------------------------------------------------------------------------------------------*/\n\nconst slugify = (s: string) =>\n  s\n    .toLowerCase()\n    .normalize(\"NFKD\")\n    .replace(/[̀-ͯ]/g, \"\")\n    .replace(/[^a-z0-9]+/g, \"-\")\n    .replace(/^-|-$/g, \"\") || \"section\";\n\nfunction enhance(root: HTMLElement, autoIds: boolean) {\n  if (autoIds) {\n    for (const h of root.querySelectorAll<HTMLElement>(\"h2:not([id]),h3:not([id]),h4:not([id])\")) {\n      const base = slugify(h.textContent ?? \"\");\n      let id = base;\n      for (let n = 2; document.getElementById(id); n++) id = `${base}-${n}`;\n      h.id = id;\n    }\n  }\n  // A scrollable region has to be reachable by keyboard, or its overflow can't be scrolled.\n  for (const pre of root.querySelectorAll<HTMLElement>(\"pre:not([tabindex])\")) pre.tabIndex = 0;\n  // display:block (for horizontal scroll) drops table semantics in some browsers; say them out loud.\n  for (const table of root.querySelectorAll<HTMLElement>(\"table:not([role])\")) {\n    table.setAttribute(\"role\", \"table\");\n    for (const g of table.querySelectorAll(\"thead,tbody,tfoot\")) g.setAttribute(\"role\", \"rowgroup\");\n    for (const tr of table.querySelectorAll(\"tr\")) tr.setAttribute(\"role\", \"row\");\n    for (const th of table.querySelectorAll(\"th\")) th.setAttribute(\"role\", th.getAttribute(\"scope\") === \"row\" ? \"rowheader\" : \"columnheader\");\n    for (const td of table.querySelectorAll(\"td\")) td.setAttribute(\"role\", \"cell\");\n    if (table.scrollWidth > table.clientWidth) table.tabIndex = 0;\n  }\n  for (const img of root.querySelectorAll<HTMLImageElement>(\"img:not([data-state])\")) {\n    if (img.complete) {\n      img.dataset.state = img.naturalWidth ? \"loaded\" : \"error\";\n      continue;\n    }\n    img.dataset.state = \"loading\";\n    img.addEventListener(\"load\", () => (img.dataset.state = \"loaded\"), { once: true });\n    img.addEventListener(\"error\", () => (img.dataset.state = \"error\"), { once: true });\n  }\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Floating controls\n * -----------------------------------------------------------------------------------------------*/\n\ntype AnchorTarget = { id: string; label: string; x: number; y: number };\ntype CodeTarget = { el: HTMLElement; top: number; right: number };\n\nconst HEADINGS = \"h2[id],h3[id],h4[id]\";\n\n// Where a heading's text actually ends (its last line), relative to the container: the link sits there.\nfunction anchorFor(h: HTMLElement, box: DOMRect): AnchorTarget {\n  const range = document.createRange();\n  range.selectNodeContents(h);\n  const rects = range.getClientRects();\n  const last = rects[rects.length - 1] ?? h.getBoundingClientRect();\n  return { id: h.id, label: h.textContent?.trim() ?? \"\", x: last.right - box.left + 6, y: last.top + last.height / 2 - box.top };\n}\n\nfunction codeFor(pre: HTMLElement, box: DOMRect): CodeTarget {\n  const r = pre.getBoundingClientRect();\n  return { el: pre, top: r.top - box.top + 6, right: box.right - r.right + 6 };\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Prose\n * -----------------------------------------------------------------------------------------------*/\n\nexport type ProseProps = Omit<React.ComponentProps<\"div\">, \"dangerouslySetInnerHTML\"> & {\n  /** \"sm\" for comments and descriptions in product UI, \"md\" for docs, \"lg\" for long reads. */\n  size?: \"sm\" | \"md\" | \"lg\";\n  /** Rendered HTML (from markdown or a CMS). Sanitize it first. Or pass children instead. */\n  html?: string;\n  /** Cap the line length at about 68 characters. */\n  measure?: boolean;\n  /** A copy-link button beside h2–h4 on hover. */\n  anchors?: boolean;\n  /** A copy button on code blocks, and ⌘C / Ctrl+C on a focused block copies all of it. */\n  copyCode?: boolean;\n  /** Give h2–h4 without an id one from their text, so they can be linked to. */\n  autoIds?: boolean;\n};\n\n/** Typography for rendered rich text: rhythm, headings, lists, code, quotes, tables and figures, in tokens. */\nexport function Prose({\n  size = \"md\",\n  html,\n  measure = true,\n  anchors = true,\n  copyCode = true,\n  autoIds = true,\n  className,\n  children,\n  onPointerOver,\n  onPointerLeave,\n  onFocus,\n  onKeyDown,\n  ...rest\n}: ProseProps) {\n  const root = useRef<HTMLDivElement>(null);\n  const body = useRef<HTMLDivElement>(null);\n  const copyRef = useRef<HTMLButtonElement>(null);\n  const [anchor, setAnchor] = useState<AnchorTarget | null>(null);\n  const [code, setCode] = useState<CodeTarget | null>(null);\n  const reduce = useReducedMotion();\n  const hideTimer = useRef<number>(undefined);\n\n  useEffect(() => {\n    const el = body.current;\n    if (!el) return;\n    enhance(el, autoIds);\n    el.dataset.enhanced = \"\";\n    let frame = 0;\n    // Content that arrives later (streamed replies, lazy sections) gets the same treatment.\n    const mo = new MutationObserver(() => {\n      cancelAnimationFrame(frame);\n      frame = requestAnimationFrame(() => enhance(el, autoIds));\n    });\n    mo.observe(el, { childList: true, subtree: true });\n    return () => {\n      mo.disconnect();\n      cancelAnimationFrame(frame);\n      window.clearTimeout(hideTimer.current);\n    };\n  }, [html, autoIds]);\n\n  // The code button marks its block, so the block's language label can step aside for it.\n  useEffect(() => {\n    if (!code) return;\n    code.el.setAttribute(\"data-prose-active\", \"\");\n    return () => code.el.removeAttribute(\"data-prose-active\");\n  }, [code]);\n\n  const show = useCallback(\n    (target: EventTarget | null) => {\n      const box = root.current?.getBoundingClientRect();\n      if (!box || !(target instanceof Element) || !body.current?.contains(target)) return;\n      window.clearTimeout(hideTimer.current);\n      const h = anchors ? target.closest<HTMLElement>(HEADINGS) : null;\n      const pre = copyCode ? target.closest<HTMLElement>(\"pre\") : null;\n      setAnchor((a) => (h ? (a?.id === h.id ? a : anchorFor(h, box)) : null));\n      setCode((c) => (pre ? (c?.el === pre ? c : codeFor(pre, box)) : null));\n    },\n    [anchors, copyCode],\n  );\n\n  // Leaving a heading or block hides its control after a beat, so the pointer can travel onto it.\n  const hideSoon = () => {\n    window.clearTimeout(hideTimer.current);\n    hideTimer.current = window.setTimeout(() => {\n      setAnchor(null);\n      setCode(null);\n    }, 160);\n  };\n  const hold = () => window.clearTimeout(hideTimer.current);\n\n  const bodyProps = html !== undefined ? { dangerouslySetInnerHTML: { __html: html } } : { children };\n  const pop = reduce\n    ? { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 } }\n    : { initial: { opacity: 0, scale: 0.9 }, animate: { opacity: 1, scale: 1 }, exit: { opacity: 0, scale: 0.95, transition: { duration: 0.1 } } };\n\n  return (\n    <>\n      <style href=\"stealth-ui-prose\" precedence=\"default\">\n        {CSS}\n      </style>\n      <div\n        ref={root}\n        data-prose=\"\"\n        data-size={size}\n        data-measure={measure || undefined}\n        className={className}\n        onPointerOver={(e) => {\n          onPointerOver?.(e);\n          if ((e.target as Element).closest?.(\"[data-prose-control]\")) return hold();\n          const inside = (e.target as Element).closest?.(`${HEADINGS},pre`);\n          if (inside) show(e.target);\n          else hideSoon();\n        }}\n        onPointerLeave={(e) => {\n          onPointerLeave?.(e);\n          hideSoon();\n        }}\n        onFocus={(e) => {\n          onFocus?.(e);\n          // A focused code block shows its copy button, so keyboard users see what ⌘C will do.\n          if (e.target instanceof HTMLElement && e.target.matches(\"pre\")) show(e.target);\n        }}\n        onKeyDown={(e) => {\n          onKeyDown?.(e);\n          const pre = e.target instanceof HTMLElement && e.target.matches(\"pre\") ? e.target : null;\n          if (!pre || !copyCode || !(e.metaKey || e.ctrlKey) || e.key.toLowerCase() !== \"c\") return;\n          // With nothing selected, copy the whole block, the way the button would.\n          if (window.getSelection()?.toString()) return;\n          e.preventDefault();\n          show(pre);\n          requestAnimationFrame(() => copyRef.current?.click());\n        }}\n        {...rest}\n      >\n        <div ref={body} data-prose-body=\"\" {...bodyProps} />\n\n        <AnimatePresence>\n          {anchor && (\n            <motion.span\n              key=\"anchor\"\n              data-prose-control=\"\"\n              {...pop}\n              transition={{ duration: 0.14, ease: ease.out }}\n              onPointerLeave={hideSoon}\n              className=\"absolute z-10 -translate-y-1/2\"\n              style={{ left: anchor.x, top: anchor.y }}\n            >\n              <SectionLink id={anchor.id} label={anchor.label} />\n            </motion.span>\n          )}\n          {code && (\n            <motion.span\n              key={`code`}\n              data-prose-control=\"\"\n              {...pop}\n              transition={{ duration: 0.14, ease: ease.out }}\n              onPointerLeave={hideSoon}\n              className=\"absolute z-10 origin-top-right\"\n              style={{ top: code.top, right: code.right }}\n            >\n              <CopyButton\n                ref={copyRef}\n                iconOnly\n                size=\"sm\"\n                label=\"Copy code\"\n                copiedLabel=\"Code copied\"\n                failedLabel=\"Couldn’t copy\"\n                // Mouse users have the button; keyboard users copy the focused block with ⌘C.\n                tabIndex={-1}\n                aria-keyshortcuts=\"Meta+C Control+C\"\n                value={() => (code.el.querySelector(\"code\") ?? code.el).textContent?.replace(/\\n$/, \"\") ?? \"\"}\n              />\n            </motion.span>\n          )}\n        </AnimatePresence>\n      </div>\n    </>\n  );\n}\n\n// Copies a link to the section. The page doesn't move: sharing is what people want from it.\nfunction SectionLink({ id, label }: { id: string; label: string }) {\n  const { state, copy } = useCopy({ timeout: 1400 });\n  const reduce = useReducedMotion();\n  const done = state === \"copied\";\n  return (\n    <>\n      <button\n        type=\"button\"\n        tabIndex={-1}\n        aria-label={`Copy link to “${label}”`}\n        onClick={() => copy(() => `${location.origin}${location.pathname}${location.search}#${id}`)}\n        className={cn(\n          \"relative grid size-6 place-items-center rounded-md text-fg-4 outline-none\",\n          \"before:absolute before:-inset-2.5 before:content-[''] pointer-fine:before:hidden\",\n          \"transition-[background-color,color,scale] duration-150 hover:bg-hover hover:text-fg-2 active:scale-[0.9] active:duration-75\",\n          done && \"text-fg-2\",\n        )}\n      >\n        <AnimatePresence initial={false} mode=\"popLayout\">\n          <motion.span\n            key={done ? \"done\" : \"link\"}\n            initial={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.5, filter: \"blur(2px)\" }}\n            animate={{ opacity: 1, scale: 1, filter: \"blur(0px)\" }}\n            exit={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.5, filter: \"blur(2px)\" }}\n            transition={{ duration: 0.16, ease: ease.out }}\n            className=\"grid place-items-center\"\n          >\n            {done ? <Check size={14} /> : <LinkIcon size={14} />}\n          </motion.span>\n        </AnimatePresence>\n      </button>\n      <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n        {done ? \"Link copied\" : state === \"failed\" ? \"Couldn’t copy the link\" : \"\"}\n      </span>\n    </>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "https://www.stealth.pm/ui/prose"
}
