{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"bracket-focus","type":"registry:ui","title":"Bracket Focus","description":"One word at a time snaps into focus inside gliding corner brackets while the rest of the sentence blurs.","dependencies":[],"registryDependencies":["utils"],"files":[{"path":"registry/ui/bracket-focus.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { useEffect, useLayoutEffect, useRef, useState, type CSSProperties } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface BracketFocusProps {\n  text: string;\n  /** Blur of unfocused words, in px. */\n  blur?: number;\n  /** Ms each word holds focus in auto mode. */\n  interval?: number;\n  /** Ms for the frame to travel and the blur to shift. */\n  duration?: number;\n  /** Cycle on its own, or follow the pointer. */\n  mode?: \"auto\" | \"hover\";\n  /** Corner bracket color. */\n  color?: string;\n  className?: string;\n}\n\nconst corner = \"absolute size-[0.3em] min-h-3 min-w-3 rounded-[3px]\";\nconst bracket: CSSProperties = { borderColor: \"var(--focus)\", filter: \"drop-shadow(0 0 6px var(--focus))\" };\n\nexport function BracketFocus({\n  text,\n  blur = 5,\n  interval = 1800,\n  duration = 550,\n  mode = \"auto\",\n  color = \"#bef264\",\n  className,\n}: BracketFocusProps) {\n  const root = useRef<HTMLDivElement>(null);\n  const frame = useRef<HTMLSpanElement>(null);\n  const placed = useRef(false);\n  const [active, setActive] = useState(0);\n  const words = text.split(/\\s+/).filter(Boolean);\n  const n = words.length;\n\n  useEffect(() => {\n    if (mode !== \"auto\" || n < 2) return;\n    const id = window.setInterval(() => setActive((a) => (a + 1) % n), interval);\n    return () => window.clearInterval(id);\n  }, [mode, n, interval]);\n\n  // Move the frame around the active word (DOM write, re-run on resize).\n  useLayoutEffect(() => {\n    const el = root.current;\n    const f = frame.current;\n    if (!el || !f) return;\n    const place = (animate: boolean) => {\n      const word = el.querySelectorAll<HTMLElement>(\"[data-word]\")[active % Math.max(1, n)];\n      if (!word) return;\n      const pad = Math.max(6, word.offsetHeight * 0.12);\n      const still = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n      f.style.transition =\n        animate && !still\n          ? `transform ${duration}ms cubic-bezier(.65,0,.35,1), width ${duration}ms cubic-bezier(.65,0,.35,1), height ${duration}ms cubic-bezier(.65,0,.35,1), opacity .3s`\n          : \"none\";\n      f.style.transform = `translate(${word.offsetLeft - pad}px, ${word.offsetTop - pad}px)`;\n      f.style.width = `${word.offsetWidth + pad * 2}px`;\n      f.style.height = `${word.offsetHeight + pad * 2}px`;\n      f.style.opacity = \"1\";\n    };\n    place(placed.current);\n    placed.current = true;\n    const ro = new ResizeObserver(() => place(false));\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, [active, n, duration, text]);\n\n  return (\n    <div\n      ref={root}\n      className={cn(\"relative inline-flex flex-wrap justify-center gap-x-[0.3em] gap-y-[0.2em]\", className)}\n      style={{ \"--focus\": color } as CSSProperties}\n    >\n      <span className=\"sr-only\">{text}</span>\n      {words.map((w, i) => {\n        const on = i === active % n;\n        return (\n          <span\n            key={i}\n            data-word\n            aria-hidden\n            onPointerEnter={mode === \"hover\" ? () => setActive(i) : undefined}\n            style={{\n              filter: on ? \"none\" : `blur(${blur}px)`,\n              opacity: on ? 1 : 0.5,\n              transition: `filter ${duration}ms ease, opacity ${duration}ms ease`,\n              cursor: mode === \"hover\" ? \"default\" : undefined,\n            }}\n          >\n            {w}\n          </span>\n        );\n      })}\n      <span ref={frame} aria-hidden className=\"pointer-events-none absolute left-0 top-0\" style={{ opacity: 0 }}>\n        <span className={cn(corner, \"left-0 top-0 border-l-[3px] border-t-[3px]\")} style={bracket} />\n        <span className={cn(corner, \"right-0 top-0 border-r-[3px] border-t-[3px]\")} style={bracket} />\n        <span className={cn(corner, \"bottom-0 left-0 border-b-[3px] border-l-[3px]\")} style={bracket} />\n        <span className={cn(corner, \"bottom-0 right-0 border-b-[3px] border-r-[3px]\")} style={bracket} />\n      </span>\n    </div>\n  );\n}\n"}]}