{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "slug-field-mirror",
  "title": "Slug Field Mirror",
  "description": "A primary field with a slugified 'carbon copy' stacked underneath it, offset like a duplicate sheet peeking out from beneath a receipt. Every keystroke stamps its transformed counterpart onto the flimsy with a tiny pressure jitter; editing the flimsy directly tears it off the coupling until a real Relink button re-stamps it.",
  "dependencies": [],
  "files": [
    {
      "path": "registry/core/slug-field-mirror/component.tsx",
      "content": "\"use client\";\n\nimport {\n  useCallback,\n  useEffect,\n  useId,\n  useRef,\n  useState,\n  type CSSProperties,\n} from \"react\";\n\n// ---------------------------------------------------------------------------\n// CarbonFlimsy — a primary text field with a \"carbon copy\" derived field\n// stacked underneath it. Every keystroke in the primary is transformed\n// (default: slugified) and stamped onto the flimsy sheet: each newly-arrived\n// character mounts fresh and plays a short pressure-jitter keyframe\n// (translate 0.4px, font-weight 600 -> 400), staggered ~15ms apart when\n// several characters land at once (paste, or a full re-stamp). The flimsy\n// sheet sits visually offset 4px down-right from the primary, like a second\n// page peeking out from underneath — while linked its border and background\n// read as a thin under-sheet (color-mix toward --ns-muted).\n//\n// Editing the flimsy directly detaches it: the offset springs back to zero,\n// the border firms to --foreground at 30%, mirroring stops, and the value\n// becomes independently editable. A \"Relink\" button (only rendered while\n// detached) restores the coupling and replays a full re-stamp sweep across\n// the freshly-derived value.\n//\n// Both fields are real, independently labelled <input> elements — the only\n// decorative-only layer is the stamp overlay, an aria-hidden row of spans\n// painted exactly over the flimsy input while its own text is transparent\n// (caret-color kept so a real caret still shows). The flimsy input always\n// carries the accessible value; the overlay never diverges from it. Detach\n// and relink each fire one polite live-region announcement. Reduced motion\n// drops the jitter and the spring transition; nothing about legibility or\n// operability depends on either.\n// ---------------------------------------------------------------------------\n\ntype Vars = CSSProperties & Record<`--${string}`, string | number>;\n\nconst CHAR_STAGGER_MS = 15;\n\nfunction defaultTransform(value: string): string {\n  return value\n    .toLowerCase()\n    .trim()\n    .replace(/[^a-z0-9]+/g, \"-\")\n    .replace(/^-+|-+$/g, \"\");\n}\n\nfunction commonPrefixLength(a: string, b: string): number {\n  const n = Math.min(a.length, b.length);\n  let i = 0;\n  while (i < n && a[i] === b[i]) i++;\n  return i;\n}\n\nfunction useReducedMotion() {\n  const [reduced, setReduced] = useState(false);\n  useEffect(() => {\n    const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    setReduced(mq.matches);\n    const onChange = () => setReduced(mq.matches);\n    mq.addEventListener(\"change\", onChange);\n    return () => mq.removeEventListener(\"change\", onChange);\n  }, []);\n  return reduced;\n}\n\ninterface Stamp {\n  text: string;\n  /** per-character \"batch\" id — a span's React key includes this, so only\n   *  characters whose gen just changed remount and replay the jitter. */\n  gen: number[];\n  /** index the current batch's newly-stamped run starts at, for stagger delay. */\n  from: number;\n}\n\nexport interface CarbonFlimsyProps {\n  /** Accessible label for the primary field, e.g. \"Project title\". */\n  label: string;\n  /** Accessible label for the derived flimsy field, e.g. \"Slug\". */\n  derivedLabel: string;\n  /** Controlled primary value. */\n  value?: string;\n  /** Initial primary value when uncontrolled. @default \"\" */\n  defaultValue?: string;\n  /** called with the new primary value on every keystroke */\n  onValueChange?: (value: string) => void;\n  /** Fires whenever the derived value changes, with whether it's still linked. */\n  onDerivedChange?: (value: string, linked: boolean) => void;\n  /** Transforms the primary value into the derived one. @default slugify */\n  transform?: (value: string) => string;\n  /** placeholder for the primary field */\n  placeholder?: string;\n  /** Shown in the flimsy field while its value is empty. @default \"auto-generated\" */\n  derivedPlaceholder?: string;\n  /** extra classes merged onto the rendered root element */\n  className?: string;\n}\n\nexport function CarbonFlimsy({\n  label,\n  derivedLabel,\n  value,\n  defaultValue = \"\",\n  onValueChange,\n  onDerivedChange,\n  transform = defaultTransform,\n  placeholder,\n  derivedPlaceholder = \"auto-generated\",\n  className = \"\",\n}: CarbonFlimsyProps) {\n  const isControlled = value !== undefined;\n  const [internalPrimary, setInternalPrimary] = useState(defaultValue);\n  const primary = isControlled ? (value as string) : internalPrimary;\n\n  const [linked, setLinked] = useState(true);\n  const [stamp, setStamp] = useState<Stamp>(() => {\n    const t = transform(defaultValue);\n    return { text: t, gen: Array.from(t, () => 0), from: 0 };\n  });\n  const [live, setLive] = useState(\"\");\n\n  const reducedMotion = useReducedMotion();\n  const stampGenRef = useRef(0);\n  const prevPrimaryRef = useRef(primary);\n  const uid = useId();\n  const primaryId = `${uid}-primary`;\n  const derivedId = `${uid}-derived`;\n  const hintId = `${uid}-hint`;\n\n  const stampDerived = useCallback((next: string, full = false) => {\n    setStamp((prev) => {\n      const prefixLen = full ? 0 : commonPrefixLength(prev.text, next);\n      stampGenRef.current += 1;\n      const gen = stampGenRef.current;\n      return {\n        text: next,\n        gen: Array.from(next, (_, i) => (i < prefixLen ? (prev.gen[i] ?? 0) : gen)),\n        from: prefixLen,\n      };\n    });\n  }, []);\n\n  // Re-derive whenever the primary value actually changes while linked —\n  // covers both the uncontrolled onChange path and an externally controlled\n  // `value` update, without duplicating the derivation logic in either handler.\n  useEffect(() => {\n    if (!linked) {\n      prevPrimaryRef.current = primary;\n      return;\n    }\n    if (primary === prevPrimaryRef.current) return;\n    prevPrimaryRef.current = primary;\n    const next = transform(primary);\n    stampDerived(next);\n    onDerivedChange?.(next, true);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [primary, linked]);\n\n  const handlePrimaryChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    const next = e.target.value;\n    if (!isControlled) setInternalPrimary(next);\n    onValueChange?.(next);\n  };\n\n  const handleFlimsyChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n    const next = e.target.value;\n    setStamp((prev) => ({ ...prev, text: next }));\n    if (linked) {\n      setLinked(false);\n      setLive(`Detached. ${derivedLabel} no longer follows ${label}.`);\n    }\n    onDerivedChange?.(next, false);\n  };\n\n  const handleRelink = () => {\n    const next = transform(primary);\n    setLinked(true);\n    stampDerived(next, true);\n    setLive(`Relinked. ${derivedLabel} restored from ${label}.`);\n    onDerivedChange?.(next, true);\n  };\n\n  return (\n    <div\n      className={`ns-cf-root flex w-full flex-col gap-1.5 ${className}`}\n      data-reduced={reducedMotion || undefined}\n    >\n      <style>{`\n.ns-cf-flimsy-box{transition:transform 340ms cubic-bezier(.34,1.56,.64,1),border-color 200ms ease,background-color 200ms ease,box-shadow 150ms ease}\n.ns-cf-flimsy-box[data-linked]{transform:translate(4px,4px);border-color:var(--border);background:color-mix(in oklab,var(--ns-muted) 14%,var(--background))}\n.ns-cf-flimsy-box[data-detached]{transform:translate(0,0);border-color:color-mix(in oklab,var(--foreground) 30%,transparent);background:var(--background)}\n.ns-cf-flimsy-box:focus-within{border-color:var(--ns-accent);box-shadow:0 0 0 3px color-mix(in oklab,var(--ns-accent) 18%,transparent)}\n.ns-cf-flimsy-input[data-linked]{color:transparent;caret-color:var(--foreground)}\n.ns-cf-char{display:inline-block;animation:ns-cf-stamp 120ms ease-out both;animation-delay:var(--cf-delay,0ms)}\n@keyframes ns-cf-stamp{0%{transform:translate(0,0);font-weight:600}45%{transform:translate(.4px,.4px);font-weight:500}100%{transform:translate(0,0);font-weight:400}}\n.ns-cf-primary-input:focus-visible,.ns-cf-flimsy-input:focus-visible{outline:2px solid var(--ns-accent);outline-offset:2px}\n.ns-cf-relink:focus-visible{outline:2px solid var(--ns-accent);outline-offset:2px}\n.ns-cf-root[data-reduced] .ns-cf-char{animation:none;font-weight:400}\n.ns-cf-root[data-reduced] .ns-cf-flimsy-box{transition:none}\n@media (prefers-reduced-motion: reduce){\n.ns-cf-char{animation:none;font-weight:400}\n.ns-cf-flimsy-box{transition:none}\n}\n`}</style>\n\n      <div className=\"flex flex-col gap-1.5\">\n        <label\n          htmlFor={primaryId}\n          className=\"text-[13px] font-medium tracking-tight text-foreground\"\n        >\n          {label}\n        </label>\n        <input\n          id={primaryId}\n          type=\"text\"\n          value={primary}\n          placeholder={placeholder}\n          onChange={handlePrimaryChange}\n          data-cf-role=\"primary\"\n          className=\"ns-cf-primary-input h-9 rounded-[6px] border border-border bg-background px-3 text-sm text-foreground outline-none transition-colors duration-150 placeholder:text-ns-muted/70 hover:border-foreground/25 focus:border-ns-accent\"\n        />\n      </div>\n\n      <div className=\"relative mt-1 pb-1 pr-1\">\n        <div\n          data-linked={linked || undefined}\n          data-detached={!linked || undefined}\n          className=\"ns-cf-flimsy-box flex flex-col gap-1 rounded-[12px] border px-3 py-2\"\n        >\n          <div className=\"flex items-center justify-between gap-2\">\n            <label\n              htmlFor={derivedId}\n              className=\"text-[11px] font-medium uppercase tracking-[0.08em] text-ns-muted\"\n            >\n              {derivedLabel}\n            </label>\n            {!linked && (\n              <button\n                type=\"button\"\n                onClick={handleRelink}\n                className=\"ns-cf-relink inline-flex items-center gap-1 rounded-[6px] px-1.5 py-0.5 text-[11px] font-medium text-ns-accent transition-colors hover:text-ns-accent-hover\"\n              >\n                <svg aria-hidden=\"true\" viewBox=\"0 0 12 12\" className=\"h-[10px] w-[10px]\">\n                  <path\n                    d=\"M4.6 4.6 1.8 7.4a1.7 1.7 0 0 0 2.4 2.4L7 7\"\n                    fill=\"none\"\n                    stroke=\"currentColor\"\n                    strokeWidth=\"1.3\"\n                    strokeLinecap=\"round\"\n                  />\n                  <path\n                    d=\"M7.4 7.4l2.8-2.8a1.7 1.7 0 0 0-2.4-2.4L5 5\"\n                    fill=\"none\"\n                    stroke=\"currentColor\"\n                    strokeWidth=\"1.3\"\n                    strokeLinecap=\"round\"\n                  />\n                </svg>\n                Relink\n              </button>\n            )}\n          </div>\n\n          <div className=\"relative\">\n            <input\n              id={derivedId}\n              type=\"text\"\n              value={stamp.text}\n              placeholder={derivedPlaceholder}\n              onChange={handleFlimsyChange}\n              aria-describedby={hintId}\n              data-linked={linked || undefined}\n              className=\"ns-cf-flimsy-input h-7 w-full border-0 bg-transparent p-0 font-mono text-[13px] leading-7 text-foreground outline-none placeholder:text-ns-muted/70\"\n            />\n            {linked && (\n              <div\n                aria-hidden=\"true\"\n                className=\"ns-cf-overlay pointer-events-none absolute inset-0 flex items-center overflow-hidden whitespace-nowrap font-mono text-[13px] leading-7 text-foreground\"\n              >\n                {Array.from(stamp.text).map((ch, i) => {\n                  const delay = i >= stamp.from ? (i - stamp.from) * CHAR_STAGGER_MS : 0;\n                  return (\n                    <span\n                      key={`${i}-${stamp.gen[i] ?? 0}`}\n                      className=\"ns-cf-char\"\n                      style={{ \"--cf-delay\": `${delay}ms` } as Vars}\n                    >\n                      {ch === \" \" ? \" \" : ch}\n                    </span>\n                  );\n                })}\n              </div>\n            )}\n          </div>\n        </div>\n      </div>\n\n      <p id={hintId} className=\"text-[12px] leading-4 text-ns-muted\">\n        Auto-generated from {label}. Editing detaches it.\n      </p>\n\n      <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n        {live}\n      </span>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "components/ui/slug-field-mirror.tsx"
    }
  ],
  "cssVars": {
    "theme": {
      "color-ns-muted": "var(--ns-muted)",
      "color-ns-accent": "var(--ns-accent)",
      "color-ns-accent-hover": "var(--ns-accent-hover)"
    },
    "light": {
      "ns-muted": "#4d4d4d",
      "ns-accent": "#006bff",
      "ns-accent-hover": "#0059d1"
    },
    "dark": {
      "ns-muted": "#8f8f8f"
    }
  },
  "meta": {
    "collection": "core",
    "tags": [
      "field",
      "slug",
      "derived-field",
      "form",
      "carbon-copy",
      "typography",
      "micro-interaction"
    ],
    "instruction": "A primary labelled text input with a second, real, independently-labelled 'flimsy' input stacked directly under it, styled as a carbon-copy sheet: 12px border-radius, 1px --border, offset translate(4px,4px) so it visually peeks out from behind the primary's bottom-right corner, background color-mix(in oklab, var(--ns-muted) 14%, var(--background)). RENDER: the flimsy input's own text is transparent (caret-color kept on --foreground so a real caret still shows) and an aria-hidden absolutely-positioned overlay of one <span> per character, in Geist Mono, paints the visible glyphs on top — the underlying real <input> always carries the true accessible value, the overlay never diverges from it, it is purely decorative. DERIVATION: every primary keystroke recomputes the flimsy value via a `transform` prop (default a kebab-case slugifier: lowercase, non-alphanumeric runs collapsed to single hyphens, trimmed) and diffs it against the previous derived string by common-prefix length; characters at or past that prefix are 'newly stamped' and remount (keyed on an incrementing per-batch generation id) so a CSS keyframe plays automatically on mount: translate(0,0)->translate(0.4px,0.4px)->translate(0,0) with font-weight 600->500->400 over 120ms, each newly-stamped character's animation-delay offset by its distance from the batch's start index times 15ms, so a single keystroke stamps instantly (0ms) while a paste or a full re-stamp sweeps left-to-right. DETACH: any onChange fired directly on the flimsy input (never fired by the programmatic mirror, only by real user input) immediately sets linked=false — the box's transform springs from translate(4px,4px) to translate(0,0) over 340ms cubic-bezier(.34,1.56,.64,1), its border firms from --border to color-mix(in oklab, var(--foreground) 30%, transparent), its background flattens to plain --background, the character overlay unmounts (the input's own text becomes visible and freely editable), and mirroring from the primary stops entirely. RELINK: a real <button>'Relink'</button> — rendered only while detached, an ordinary tabbable control, never the first interactive element at rest — recomputes the transform from the current primary value, sets linked=true (springing the offset/border/background back), and calls the stamp path with `full=true`, forcing every character of the fresh value to remount and sweep in together (prefixLen=0), reading as the sheet being re-inserted and stamped fresh. A11Y: both inputs have real <label for> bindings; the flimsy input carries aria-describedby pointing at a persistent helper paragraph reading 'Auto-generated from {label}. Editing detaches it.'; detach and relink each fire exactly one message into a role=status aria-live=polite region (not on every keystroke); the character jitter is purely visual and never affects the accessible value; Tab reaches the primary input, then the flimsy input, then Relink when it exists. REDUCED MOTION: prefers-reduced-motion (media query and a mirrored data-reduced attribute) drops the character keyframe entirely (characters just appear at 400 weight) and removes the box's spring transition, so detach/relink still change state instantly and legibly without any motion."
  },
  "type": "registry:ui"
}