{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "type-specimen",
  "type": "registry:ui",
  "title": "Type Specimen",
  "description": "An interactive variable-font specimen: editable sample, live axis sliders, waterfall, glyph loupe and CSS output.",
  "author": "WANSTUDIO <https://wanstudio.my>",
  "dependencies": [],
  "files": [
    {
      "path": "registry/wancraft/type-specimen.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nfunction subscribeReducedMotion(callback: () => void) {\n  const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n  mq.addEventListener(\"change\", callback);\n  return () => mq.removeEventListener(\"change\", callback);\n}\n\n/** SSR-safe: hydrates as \"no preference\", then follows the media query. */\nfunction usePrefersReducedMotion() {\n  return React.useSyncExternalStore(\n    subscribeReducedMotion,\n    () => window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches,\n    () => false,\n  );\n}\n\nexport interface TypeAxis {\n  /** Four-letter axis tag, e.g. \"wght\", \"wdth\", \"slnt\", \"opsz\". */\n  tag: string;\n  /** Human name shown on the slider, e.g. \"Weight\". */\n  name: string;\n  min: number;\n  max: number;\n  default: number;\n  /** Slider step; defaults to 1 for wide ranges and 0.1 for narrow ones. */\n  step?: number;\n}\n\nexport interface TypeSpecimenProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"children\"> {\n  /** CSS font-family used to render the specimen (e.g. \"var(--font-display)\" or '\"Inter\", sans-serif'). */\n  fontFamily: string;\n  /** Display name, also used in the CSS snippet. Defaults to `fontFamily`. */\n  fontName?: string;\n  /** Variable axes, each rendered as a labelled slider. */\n  axes?: TypeAxis[];\n  /** Initial sample text (editable). */\n  sample?: string;\n  /** Initial font size in px. */\n  size?: number;\n  /** Initial line height (unitless). */\n  leading?: number;\n  /** Initial letter spacing in em. */\n  tracking?: number;\n  /** Show the size waterfall tab. */\n  showWaterfall?: boolean;\n  /** Show the character set tab. */\n  showGlyphs?: boolean;\n  /** Show the CSS snippet tab. */\n  showCss?: boolean;\n  /** Sizes for the waterfall, in px. */\n  waterfall?: number[];\n  /** Characters shown in the character set grid. */\n  characters?: string;\n  /** Slider, focus and highlight colour (any CSS colour). */\n  accent?: string;\n}\n\ntype Tab = \"waterfall\" | \"glyphs\" | \"css\";\n\nconst DEFAULT_CHARACTERS = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789&@?!%#$€£¥(){}[]/*+=<>\";\nconst DEFAULT_WATERFALL = [12, 16, 24, 36, 48, 64];\n\nconst round = (n: number, step = 0.01) => Number((Math.round(n / step) * step).toFixed(4));\nconst stepFor = (axis: TypeAxis) => axis.step ?? (axis.max - axis.min > 20 ? 1 : 0.1);\nconst codepoint = (ch: string) => `U+${(ch.codePointAt(0) ?? 0).toString(16).toUpperCase().padStart(4, \"0\")}`;\n\nfunction Slider({\n  id,\n  label,\n  tag,\n  value,\n  min,\n  max,\n  step,\n  unit = \"\",\n  disabled,\n  onChange,\n}: {\n  id: string;\n  label: string;\n  tag?: string;\n  value: number;\n  min: number;\n  max: number;\n  step: number;\n  unit?: string;\n  disabled?: boolean;\n  onChange: (value: number) => void;\n}) {\n  const pct = ((value - min) / (max - min)) * 100;\n  const display = `${round(value, step < 1 ? Math.min(step, 0.01) : 1)}${unit}`;\n  return (\n    <div className={disabled ? \"opacity-45\" : \"\"}>\n      <div className=\"flex items-baseline justify-between gap-3\">\n        <label htmlFor={id} className=\"flex items-baseline gap-2 text-[12px] text-zinc-700 dark:text-zinc-300\">\n          {label}\n          {tag && <span className=\"font-mono text-[10px] uppercase tracking-[0.14em] text-zinc-400 dark:text-zinc-500\">{tag}</span>}\n        </label>\n        <output htmlFor={id} className=\"font-mono text-[11px] tabular-nums text-zinc-500 dark:text-zinc-400\">\n          {display}\n        </output>\n      </div>\n      <input\n        id={id}\n        type=\"range\"\n        min={min}\n        max={max}\n        step={step}\n        value={value}\n        disabled={disabled}\n        aria-valuetext={display}\n        onChange={(e) => onChange(Number(e.currentTarget.value))}\n        style={{ [\"--fill\" as string]: `${Math.round(pct * 100) / 100}%` }}\n        className=\"ts-range mt-1 block w-full rounded-full outline-hidden focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-[var(--ts-accent)] disabled:cursor-not-allowed\"\n      />\n    </div>\n  );\n}\n\n/**\n * An interactive specimen for a variable font: an editable sample, a labelled slider per axis plus\n * size, leading and tracking, a size waterfall, a character set with a magnifying loupe and the\n * CSS for the current settings, ready to copy.\n */\nexport function TypeSpecimen({\n  fontFamily,\n  fontName,\n  axes = [],\n  sample = \"Sphinx of black quartz, judge my vow\",\n  size = 72,\n  leading = 1.05,\n  tracking = -0.02,\n  showWaterfall = true,\n  showGlyphs = true,\n  showCss = true,\n  waterfall = DEFAULT_WATERFALL,\n  characters = DEFAULT_CHARACTERS,\n  accent = \"#FF7448\",\n  className = \"\",\n  style,\n  ...props\n}: TypeSpecimenProps) {\n  const reduceMotion = usePrefersReducedMotion();\n  const uid = React.useId();\n  const id = (part: string) => `ts-${uid}-${part}`;\n\n  const axesKey = axes.map((a) => `${a.tag}:${a.min}:${a.max}:${a.default}`).join(\"|\");\n  const axisDefaults = () => Object.fromEntries(axes.map((a) => [a.tag, a.default]));\n  const hasOpsz = axes.some((a) => a.tag === \"opsz\");\n\n  const [text, setText] = React.useState(sample);\n  const [fontSize, setFontSize] = React.useState(size);\n  const [lineHeight, setLineHeight] = React.useState(leading);\n  const [letterSpacing, setLetterSpacing] = React.useState(tracking);\n  const [axisValues, setAxisValues] = React.useState<Record<string, number>>(axisDefaults);\n  const [linkOpsz, setLinkOpsz] = React.useState(true);\n  const [tab, setTab] = React.useState<Tab | null>(null);\n  const [glyph, setGlyph] = React.useState(0);\n  const [copied, setCopied] = React.useState(false);\n\n  // Adopt new initial values when the props change (e.g. switching fonts), without an effect.\n  const [synced, setSynced] = React.useState({ sample, size, leading, tracking, axesKey });\n  if (synced.sample !== sample || synced.size !== size || synced.leading !== leading || synced.tracking !== tracking || synced.axesKey !== axesKey) {\n    setSynced({ sample, size, leading, tracking, axesKey });\n    if (synced.sample !== sample) setText(sample);\n    if (synced.size !== size) setFontSize(size);\n    if (synced.leading !== leading) setLineHeight(leading);\n    if (synced.tracking !== tracking) setLetterSpacing(tracking);\n    if (synced.axesKey !== axesKey) setAxisValues(axisDefaults());\n  }\n\n  const textareaRef = React.useRef<HTMLTextAreaElement>(null);\n  const gridRef = React.useRef<HTMLDivElement>(null);\n  const copyTimer = React.useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n\n  const tabs: { id: Tab; label: string }[] = [\n    ...(showWaterfall ? [{ id: \"waterfall\" as const, label: \"Waterfall\" }] : []),\n    ...(showGlyphs ? [{ id: \"glyphs\" as const, label: \"Characters\" }] : []),\n    ...(showCss ? [{ id: \"css\" as const, label: \"CSS\" }] : []),\n  ];\n  const activeTab = tabs.find((t) => t.id === tab)?.id ?? tabs[0]?.id ?? null;\n\n  /* -------------------------------- settings -------------------------------- */\n\n  const valueOf = (axis: TypeAxis) => {\n    if (axis.tag === \"opsz\" && linkOpsz) return Math.min(axis.max, Math.max(axis.min, Math.round(fontSize)));\n    return axisValues[axis.tag] ?? axis.default;\n  };\n  const variation = axes.map((a) => `\"${a.tag}\" ${round(valueOf(a), stepFor(a) < 1 ? 0.1 : 1)}`).join(\", \");\n  const typeStyle: React.CSSProperties = {\n    fontFamily,\n    fontVariationSettings: variation || undefined,\n    // Optical sizing is driven explicitly through the opsz axis.\n    fontOpticalSizing: hasOpsz ? \"none\" : undefined,\n  };\n  const sampleStyle: React.CSSProperties = {\n    ...typeStyle,\n    fontSize: `min(${fontSize}px, 15cqw)`,\n    lineHeight,\n    letterSpacing: `${letterSpacing}em`,\n    transition: reduceMotion ? \"none\" : \"font-variation-settings 140ms ease-out, letter-spacing 140ms ease-out\",\n  };\n\n  const family = fontName ? `\"${fontName}\", ${/mono/i.test(fontName) ? \"monospace\" : \"sans-serif\"}` : fontFamily;\n  const cssLines = [\n    `font-family: ${family};`,\n    `font-size: ${fontSize}px;`,\n    `line-height: ${round(lineHeight)};`,\n    `letter-spacing: ${round(letterSpacing)}em;`,\n    ...(variation ? [`font-variation-settings: ${variation};`] : []),\n  ];\n  const css = `.specimen {\\n${cssLines.map((l) => `  ${l}`).join(\"\\n\")}\\n}`;\n\n  const reset = () => {\n    setText(sample);\n    setFontSize(size);\n    setLineHeight(leading);\n    setLetterSpacing(tracking);\n    setAxisValues(axisDefaults());\n    setLinkOpsz(true);\n  };\n\n  const copy = async () => {\n    try {\n      await navigator.clipboard.writeText(css);\n      setCopied(true);\n      clearTimeout(copyTimer.current);\n      copyTimer.current = setTimeout(() => setCopied(false), 1600);\n    } catch {\n      setCopied(false);\n    }\n  };\n  React.useEffect(() => () => clearTimeout(copyTimer.current), []);\n\n  // Grow the sample field with its content (field-sizing is not available everywhere).\n  React.useLayoutEffect(() => {\n    const el = textareaRef.current;\n    if (!el) return;\n    el.style.height = \"auto\";\n    el.style.height = `${el.scrollHeight}px`;\n  });\n  React.useEffect(() => {\n    const el = textareaRef.current;\n    if (!el) return;\n    const fit = () => {\n      el.style.height = \"auto\";\n      el.style.height = `${el.scrollHeight}px`;\n    };\n    const ro = new ResizeObserver(fit);\n    ro.observe(el);\n    document.fonts?.ready.then(fit);\n    return () => ro.disconnect();\n  }, []);\n\n  /* --------------------------------- glyphs --------------------------------- */\n\n  const glyphs = Array.from(characters);\n  const activeGlyph = Math.min(glyph, Math.max(0, glyphs.length - 1));\n\n  const onGlyphKey = (event: React.KeyboardEvent<HTMLButtonElement>, index: number) => {\n    const grid = gridRef.current;\n    const columns = grid ? getComputedStyle(grid).gridTemplateColumns.split(\" \").length : 8;\n    let next = index;\n    if (event.key === \"ArrowRight\") next = index + 1;\n    else if (event.key === \"ArrowLeft\") next = index - 1;\n    else if (event.key === \"ArrowDown\") next = index + columns;\n    else if (event.key === \"ArrowUp\") next = index - columns;\n    else if (event.key === \"Home\") next = 0;\n    else if (event.key === \"End\") next = glyphs.length - 1;\n    else return;\n    event.preventDefault();\n    next = Math.min(glyphs.length - 1, Math.max(0, next));\n    setGlyph(next);\n    grid?.querySelector<HTMLButtonElement>(`[data-index=\"${next}\"]`)?.focus();\n  };\n\n  const onTabKey = (event: React.KeyboardEvent<HTMLButtonElement>) => {\n    const i = tabs.findIndex((t) => t.id === activeTab);\n    let next = -1;\n    if (event.key === \"ArrowRight\") next = (i + 1) % tabs.length;\n    else if (event.key === \"ArrowLeft\") next = (i - 1 + tabs.length) % tabs.length;\n    else if (event.key === \"Home\") next = 0;\n    else if (event.key === \"End\") next = tabs.length - 1;\n    if (next === -1) return;\n    event.preventDefault();\n    setTab(tabs[next].id);\n    document.getElementById(id(`tab-${tabs[next].id}`))?.focus();\n  };\n\n  /* ---------------------------------- render --------------------------------- */\n\n  const mono = \"font-mono text-[10px] uppercase tracking-[0.14em] text-zinc-500 dark:text-zinc-400\";\n\n  return (\n    <div\n      {...props}\n      className={`@container w-full overflow-hidden rounded-2xl border border-zinc-900/10 bg-white text-zinc-950 [--ts-thumb:#ffffff] [--ts-track:rgba(24,24,27,0.12)] dark:border-white/10 dark:bg-zinc-950 dark:text-zinc-50 dark:[--ts-thumb:#18181b] dark:[--ts-track:rgba(255,255,255,0.14)] ${className}`}\n      style={{ [\"--ts-accent\" as string]: accent, ...style }}\n    >\n      <style>{`.ts-range{-webkit-appearance:none;appearance:none;height:18px;background:transparent;cursor:pointer}\n.ts-range::-webkit-slider-runnable-track{height:2px;border-radius:2px;background:linear-gradient(to right,var(--ts-accent) var(--fill),var(--ts-track) var(--fill))}\n.ts-range::-moz-range-track{height:2px;border-radius:2px;background:linear-gradient(to right,var(--ts-accent) var(--fill),var(--ts-track) var(--fill))}\n.ts-range::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;margin-top:-6px;width:14px;height:14px;border-radius:999px;background:var(--ts-thumb);border:1.5px solid var(--ts-accent);box-shadow:0 1px 4px rgba(0,0,0,.18)}\n.ts-range::-moz-range-thumb{width:12px;height:12px;border-radius:999px;background:var(--ts-thumb);border:1.5px solid var(--ts-accent);box-shadow:0 1px 4px rgba(0,0,0,.18)}`}</style>\n\n      {/* Header */}\n      <div className=\"flex items-center justify-between gap-4 border-b border-zinc-900/[0.07] px-5 py-3 dark:border-white/[0.07]\">\n        <div className=\"flex min-w-0 items-baseline gap-3\">\n          <span className=\"truncate text-[15px] font-semibold tracking-[-0.01em]\" style={{ fontFamily }}>\n            {fontName ?? fontFamily}\n          </span>\n          <span className={`${mono} hidden @[420px]:inline`}>\n            {axes.length ? `Variable · ${axes.length} ${axes.length === 1 ? \"axis\" : \"axes\"}` : \"Static\"}\n          </span>\n        </div>\n        <button\n          type=\"button\"\n          onClick={reset}\n          className={`${mono} shrink-0 rounded-full border border-zinc-900/15 px-3 py-1.5 outline-hidden transition-colors hover:border-zinc-900/40 hover:text-zinc-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-[var(--ts-accent)] dark:border-white/15 dark:hover:border-white/40 dark:hover:text-white`}\n        >\n          Reset\n        </button>\n      </div>\n\n      {/* Sample + controls */}\n      <div className=\"grid grid-cols-1 @[720px]:grid-cols-[minmax(0,1fr)_260px]\">\n        <div className=\"min-w-0 px-5 py-6 @[720px]:px-7 @[720px]:py-8\">\n          <label htmlFor={id(\"sample\")} className=\"sr-only\">\n            Sample text\n          </label>\n          <textarea\n            ref={textareaRef}\n            id={id(\"sample\")}\n            rows={1}\n            value={text}\n            spellCheck={false}\n            onChange={(e) => setText(e.currentTarget.value)}\n            style={sampleStyle}\n            className=\"block w-full resize-none overflow-hidden bg-transparent text-zinc-950 caret-[var(--ts-accent)] outline-hidden [field-sizing:content] selection:bg-[color-mix(in_srgb,var(--ts-accent)_30%,transparent)] dark:text-zinc-50\"\n          />\n          <p className={`${mono} mt-4`} aria-hidden=\"true\">\n            Click the text to edit\n          </p>\n        </div>\n\n        <div\n          role=\"group\"\n          aria-label=\"Type settings\"\n          className=\"space-y-4 border-t border-zinc-900/[0.07] px-5 py-5 @[720px]:border-l @[720px]:border-t-0 dark:border-white/[0.07]\"\n        >\n          {axes.length > 0 && <div className={mono}>Axes</div>}\n          {axes.map((axis) => (\n            <div key={axis.tag}>\n              <Slider\n                id={id(`axis-${axis.tag}`)}\n                label={axis.name}\n                tag={axis.tag}\n                value={valueOf(axis)}\n                min={axis.min}\n                max={axis.max}\n                step={stepFor(axis)}\n                disabled={axis.tag === \"opsz\" && linkOpsz}\n                onChange={(v) => setAxisValues((prev) => ({ ...prev, [axis.tag]: v }))}\n              />\n              {axis.tag === \"opsz\" && (\n                <label className=\"mt-1.5 flex cursor-pointer items-center gap-2 text-[11px] text-zinc-500 dark:text-zinc-400\">\n                  <input\n                    type=\"checkbox\"\n                    checked={linkOpsz}\n                    onChange={(e) => {\n                      const on = e.currentTarget.checked;\n                      if (!on) setAxisValues((prev) => ({ ...prev, opsz: valueOf(axis) }));\n                      setLinkOpsz(on);\n                    }}\n                    className=\"size-3.5 rounded accent-[var(--ts-accent)] outline-hidden focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-[var(--ts-accent)]\"\n                  />\n                  Match optical size to font size\n                </label>\n              )}\n            </div>\n          ))}\n          <div className={`${mono} ${axes.length ? \"pt-2\" : \"\"}`}>Setting</div>\n          <Slider id={id(\"size\")} label=\"Size\" value={fontSize} min={12} max={200} step={1} unit=\"px\" onChange={setFontSize} />\n          <Slider id={id(\"leading\")} label=\"Leading\" value={lineHeight} min={0.7} max={2} step={0.01} onChange={setLineHeight} />\n          <Slider id={id(\"tracking\")} label=\"Tracking\" value={letterSpacing} min={-0.1} max={0.2} step={0.005} unit=\"em\" onChange={setLetterSpacing} />\n        </div>\n      </div>\n\n      {/* Tabs: waterfall / characters / css */}\n      {activeTab && (\n        <div className=\"border-t border-zinc-900/[0.07] dark:border-white/[0.07]\">\n          <div role=\"tablist\" aria-label=\"Specimen views\" className=\"flex gap-1 px-4 pt-3\">\n            {tabs.map((t) => (\n              <button\n                key={t.id}\n                id={id(`tab-${t.id}`)}\n                type=\"button\"\n                role=\"tab\"\n                aria-selected={activeTab === t.id}\n                aria-controls={id(`panel-${t.id}`)}\n                tabIndex={activeTab === t.id ? 0 : -1}\n                onClick={() => setTab(t.id)}\n                onKeyDown={onTabKey}\n                className={`rounded-full px-3 py-1.5 font-mono text-[10px] uppercase tracking-[0.14em] outline-hidden transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-[var(--ts-accent)] ${\n                  activeTab === t.id\n                    ? \"bg-zinc-900 text-white dark:bg-white dark:text-zinc-950\"\n                    : \"text-zinc-500 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white\"\n                }`}\n              >\n                {t.label}\n              </button>\n            ))}\n          </div>\n\n          <div\n            id={id(`panel-${activeTab}`)}\n            role=\"tabpanel\"\n            aria-labelledby={id(`tab-${activeTab}`)}\n            tabIndex={activeTab === \"waterfall\" ? 0 : -1}\n            className=\"px-5 pb-5 pt-4 outline-hidden focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-solid focus-visible:outline-[var(--ts-accent)]\"\n          >\n            {activeTab === \"waterfall\" && (\n              <ul className=\"space-y-2\">\n                {waterfall.map((px) => (\n                  <li key={px} className=\"flex items-baseline gap-4 border-b border-zinc-900/[0.05] pb-2 last:border-0 dark:border-white/[0.05]\">\n                    <span className={`${mono} w-10 shrink-0 tabular-nums`}>{px}px</span>\n                    <span\n                      className=\"min-w-0 flex-1 truncate\"\n                      style={{ ...typeStyle, fontSize: px, lineHeight: 1.2, letterSpacing: `${letterSpacing}em` }}\n                    >\n                      {text.split(\"\\n\")[0] || sample}\n                    </span>\n                  </li>\n                ))}\n              </ul>\n            )}\n\n            {activeTab === \"glyphs\" && (\n              <div className=\"grid grid-cols-1 gap-4 @[560px]:grid-cols-[150px_minmax(0,1fr)]\">\n                {/* Loupe: the hovered / focused / selected glyph, magnified. */}\n                <div\n                  aria-live=\"polite\"\n                  className=\"flex items-center gap-4 rounded-xl border border-zinc-900/10 p-3 @[560px]:flex-col @[560px]:items-stretch dark:border-white/10\"\n                >\n                  <div\n                    aria-hidden=\"true\"\n                    className=\"relative grid aspect-square w-20 shrink-0 place-items-center overflow-hidden rounded-lg bg-[repeating-linear-gradient(to_bottom,transparent_0_19px,color-mix(in_srgb,var(--ts-accent)_22%,transparent)_19px_20px)] text-[56px] leading-none @[560px]:w-full @[560px]:text-[88px]\"\n                    style={typeStyle}\n                  >\n                    {glyphs[activeGlyph]}\n                  </div>\n                  <div className=\"min-w-0\">\n                    <span className=\"sr-only\">Character </span>\n                    <span className=\"block font-mono text-[12px] tabular-nums text-zinc-900 dark:text-zinc-100\">{codepoint(glyphs[activeGlyph] ?? \" \")}</span>\n                    <span className={`${mono} block`}>\n                      {activeGlyph + 1} of {glyphs.length}\n                    </span>\n                  </div>\n                </div>\n                <div\n                  ref={gridRef}\n                  role=\"group\"\n                  aria-label=\"Character set\"\n                  className=\"grid grid-cols-[repeat(auto-fill,minmax(38px,1fr))] gap-px overflow-hidden rounded-xl border border-zinc-900/10 bg-zinc-900/10 dark:border-white/10 dark:bg-white/10\"\n                >\n                  {glyphs.map((ch, i) => (\n                    <button\n                      key={`${ch}-${i}`}\n                      type=\"button\"\n                      data-index={i}\n                      tabIndex={i === activeGlyph ? 0 : -1}\n                      aria-label={`${ch}, ${codepoint(ch)}`}\n                      aria-pressed={i === activeGlyph}\n                      onClick={() => setGlyph(i)}\n                      onFocus={() => setGlyph(i)}\n                      onPointerEnter={(e) => {\n                        if (e.pointerType === \"mouse\") setGlyph(i);\n                      }}\n                      onKeyDown={(e) => onGlyphKey(e, i)}\n                      style={typeStyle}\n                      className={`relative grid aspect-square place-items-center bg-white text-[20px] leading-none outline-hidden transition-transform duration-150 motion-reduce:transition-none focus-visible:z-10 focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-[var(--ts-accent)] hover:z-10 hover:scale-[1.45] hover:rounded-md hover:shadow-[0_8px_24px_-10px_rgba(0,0,0,0.45)] dark:bg-zinc-950 ${\n                        i === activeGlyph ? \"text-[var(--ts-accent)]\" : \"text-zinc-800 dark:text-zinc-200\"\n                      }`}\n                    >\n                      <span aria-hidden=\"true\">{ch}</span>\n                    </button>\n                  ))}\n                </div>\n              </div>\n            )}\n\n            {activeTab === \"css\" && (\n              <div className=\"relative\">\n                <pre className=\"overflow-x-auto rounded-xl border border-zinc-900/10 bg-zinc-900/[0.03] p-4 font-mono text-[12px] leading-relaxed text-zinc-700 dark:border-white/10 dark:bg-white/[0.03] dark:text-zinc-300\">\n                  <code>\n                    <span className=\"text-zinc-500 dark:text-zinc-400\">.specimen</span> {\"{\"}\n                    {\"\\n\"}\n                    {cssLines.map((line) => {\n                      const at = line.indexOf(\":\");\n                      return (\n                        <React.Fragment key={line}>\n                          {\"  \"}\n                          <span className=\"text-[color-mix(in_srgb,var(--ts-accent)_70%,currentColor)]\">{line.slice(0, at)}</span>\n                          {line.slice(at)}\n                          {\"\\n\"}\n                        </React.Fragment>\n                      );\n                    })}\n                    {\"}\"}\n                  </code>\n                </pre>\n                <button\n                  type=\"button\"\n                  onClick={copy}\n                  className={`${mono} absolute right-2.5 top-2.5 rounded-full border border-zinc-900/15 bg-white px-3 py-1.5 outline-hidden transition-colors hover:border-zinc-900/40 hover:text-zinc-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-[var(--ts-accent)] dark:border-white/15 dark:bg-zinc-950 dark:hover:border-white/40 dark:hover:text-white`}\n                >\n                  {copied ? \"Copied\" : \"Copy CSS\"}\n                </button>\n                <span className=\"sr-only\" aria-live=\"polite\">\n                  {copied ? \"CSS copied to clipboard.\" : \"\"}\n                </span>\n              </div>\n            )}\n          </div>\n        </div>\n      )}\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "text"
  ],
  "docs": "Documentation: https://wancraft.my/components/type-specimen"
}
