{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"tick-nav","type":"registry:ui","title":"TickNav","description":"A navigation list on a ruler of tick marks: every line the pointer crosses plucks like a string, neighbouring lines and ticks stretch and light up on springs, and an optional preview card follows the pointer.","author":"fancy-ui <https://fancy-ui.rama.app>","categories":["navigation"],"registryDependencies":["https://fancy-ui.rama.app/r/internals.json","https://fancy-ui.rama.app/r/internals-dom.json","https://fancy-ui.rama.app/r/internals-motion.json","https://fancy-ui.rama.app/r/sound.json","https://fancy-ui.rama.app/r/utils.json"],"files":[{"path":"react/src/components/tick-nav/TickNav.tsx","content":"\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport type { MouseEvent as ReactMouseEvent, ReactNode } from \"react\";\nimport { cn } from \"../../utils\";\nimport { sound as soundFx } from \"../../sound/sound\";\nimport { useReducedMotion } from \"../../internals/motion/media-query\";\nimport { useElementRef } from \"../../internals/dom/use-element-ref\";\nimport { useLiveRef } from \"../../internals/dom/use-live-ref\";\nimport { createTickNavCore, TICK_NAV_DEFAULTS, TICK_NAV_FLAT_PATH } from \"./tick-nav-core\";\nimport {\n\tresolveCssColor,\n\tstartTickNavHdr,\n\ttickNavLights,\n\twatchTickNavAccent,\n\tTICK_NAV_HDR_PAD,\n} from \"./tick-nav-hdr\";\nimport type { TickNavCore } from \"./tick-nav-core\";\nimport type { TickNavItem } from \"./types\";\nimport \"./tick-nav.css\";\n\nexport interface TickNavProps {\n\t/** The rows, top to bottom. */\n\titems: TickNavItem[];\n\t/** Id of the current item. */\n\tvalue?: string;\n\t/** Called with the id of an item that is activated and not already current. */\n\tonValueChange?: (id: string) => void;\n\t/**\n\t * Content of the card that follows the pointer while a row is hovered (or\n\t * sits beside a keyboard-focused row). Leave it out and no card is rendered.\n\t */\n\tpreview?: (item: TickNavItem) => ReactNode;\n\t/** Rows over which the neighbours of the hovered row still react. */\n\treach?: number;\n\t/** Ruler ticks drawn between two rows. */\n\tminorTicks?: number;\n\t/**\n\t * Neon only: on a display that reports HDR, draw the light in a WebGPU\n\t * canvas with extended tone mapping, so the tubes run brighter than SDR\n\t * white and the beam peaks around 1000 nits. Elsewhere the CSS glow\n\t * stays on its own.\n\t */\n\thdr?: boolean;\n\t/** Accessible name of the `<nav>` landmark. */\n\tariaLabel?: string;\n\t/**\n\t * Plays a cue each time a mouse or pen plucks a line: `hover` for an\n\t * item's line, a softer `tick` for a ruler tick. Off by default; only\n\t * audible once the user has enabled sound.\n\t */\n\tsound?: boolean;\n\t/**\n\t * `\"neon\"` turns the lines into glowing tubes: proximity raises the glow,\n\t * a hot beam of light rides every pluck, labels glow on hover, and the\n\t * current line breathes. Made for dark surfaces.\n\t */\n\tvariant?: \"default\" | \"neon\";\n\t/** Additional CSS classes on the `<nav>`. */\n\tclassName?: string;\n}\n\n/**\n * A navigation list on a ruler of tick marks. All motion lives in the shared\n * core, which owns the pointer listeners and writes its CSS variables straight\n * to the DOM; React only tracks which row is hovered.\n */\nexport function TickNav({\n\titems,\n\tvalue: valueProp,\n\tonValueChange,\n\tpreview,\n\treach = TICK_NAV_DEFAULTS.reach,\n\tminorTicks = 2,\n\tariaLabel = \"Navigation\",\n\tsound = false,\n\tvariant = \"default\",\n\thdr = true,\n\tclassName,\n}: TickNavProps) {\n\t// `value` mirrors the Svelte side's `$bindable()`: the prop seeds internal\n\t// state, activation writes that state, and a re-passed NEW prop value resyncs it.\n\tconst [value, setValue] = useState(valueProp);\n\tconst [seededFrom, setSeededFrom] = useState(valueProp);\n\tif (valueProp !== seededFrom) {\n\t\tsetSeededFrom(valueProp);\n\t\tsetValue(valueProp);\n\t}\n\n\tconst [root, rootRef] = useElementRef<HTMLElement>();\n\tconst [list, listRef] = useElementRef<HTMLUListElement>();\n\t/** Row under the pointer or keyboard focus; -1 when none. */\n\tconst [active, setActive] = useState(-1);\n\tconst [core, setCore] = useState<TickNavCore | null>(null);\n\tconst [hdrCanvas, hdrCanvasRef] = useElementRef<HTMLCanvasElement>();\n\tconst [hdrOn, setHdrOn] = useState(false);\n\n\tconst reduced = useReducedMotion();\n\n\tconst ticks = Math.max(0, Math.round(minorTicks));\n\tconst currentIndex = items.findIndex((item) => item.id === value);\n\tconst activeItem = active >= 0 ? items[active] : undefined;\n\n\tconst live = useLiveRef({ reach, currentIndex, reduced, sound });\n\n\tuseEffect(() => {\n\t\tif (!root || !list) return;\n\t\tconst { reach, currentIndex, reduced } = live.current;\n\t\tconst instance = createTickNavCore(root, list, {\n\t\t\treach,\n\t\t\tcurrent: currentIndex,\n\t\t\treducedMotion: reduced,\n\t\t\tonActiveChange(index) {\n\t\t\t\tsetActive(index);\n\t\t\t},\n\t\t\tonPluck(kind) {\n\t\t\t\tif (!live.current.sound) return;\n\t\t\t\tif (kind === \"row\") soundFx.play(\"hover\");\n\t\t\t\telse soundFx.play(\"tick\", { volume: 0.6 });\n\t\t\t},\n\t\t});\n\t\tsetCore(instance);\n\t\treturn () => {\n\t\t\tinstance.destroy();\n\t\t\tsetCore(null);\n\t\t\tsetActive(-1);\n\t\t};\n\t}, [root, list, live]);\n\n\tuseEffect(() => {\n\t\tcore?.update({ reach, current: currentIndex, reducedMotion: reduced });\n\t}, [core, reach, currentIndex, reduced]);\n\n\t// Rows were added, removed or reordered, or their tick and beam paths\n\t// changed: the core re-reads them after the DOM update.\n\tconst idKey = items.map((item) => item.id).join(\"\\u0000\");\n\tuseEffect(() => {\n\t\tcore?.refresh();\n\t}, [core, idKey, ticks, variant]);\n\n\t// HDR light: async WebGPU start; resolves to null off HDR displays.\n\tuseEffect(() => {\n\t\tif (!hdrCanvas || !core || !root) return;\n\t\tconst canvas = hdrCanvas;\n\t\tconst instance = core;\n\t\tconst host = root;\n\t\tlet cancelled = false;\n\t\tlet stop: (() => void) | null = null;\n\t\tvoid startTickNavHdr(canvas).then((light) => {\n\t\t\tif (!light) return;\n\t\t\tif (cancelled) {\n\t\t\t\tlight.destroy();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Follows the theme: a new accent repaints the light at once.\n\t\t\tconst unwatch = watchTickNavAccent(canvas, (css) => {\n\t\t\t\tconst accent = resolveCssColor(css);\n\t\t\t\tif (!accent) return;\n\t\t\t\tlight.setAccent(accent);\n\t\t\t\tinstance.measure();\n\t\t\t});\n\t\t\tconst size = () =>\n\t\t\t\tlight.resize(\n\t\t\t\t\thost.offsetWidth + TICK_NAV_HDR_PAD * 2,\n\t\t\t\t\thost.offsetHeight + TICK_NAV_HDR_PAD * 2\n\t\t\t\t);\n\t\t\tsize();\n\t\t\tconst ro = new ResizeObserver(() => {\n\t\t\t\tsize();\n\t\t\t\tinstance.measure();\n\t\t\t});\n\t\t\tro.observe(host);\n\t\t\tinstance.update({ onFrame: (lines) => light.render(tickNavLights(lines)) });\n\t\t\tsetHdrOn(true);\n\t\t\tstop = () => {\n\t\t\t\tunwatch();\n\t\t\t\tro.disconnect();\n\t\t\t\tinstance.update({ onFrame: undefined });\n\t\t\t\tlight.destroy();\n\t\t\t\tsetHdrOn(false);\n\t\t\t};\n\t\t});\n\t\treturn () => {\n\t\t\tcancelled = true;\n\t\t\tstop?.();\n\t\t};\n\t}, [hdrCanvas, core, root]);\n\n\tfunction activate(item: TickNavItem, event: ReactMouseEvent): void {\n\t\tif (item.disabled) {\n\t\t\tevent.preventDefault();\n\t\t\treturn;\n\t\t}\n\t\tif (item.id === value) return;\n\t\tsetValue(item.id);\n\t\tonValueChange?.(item.id);\n\t}\n\n\treturn (\n\t\t<nav\n\t\t\tref={rootRef}\n\t\t\taria-label={ariaLabel}\n\t\t\tclassName={cn(\"tn-root\", className)}\n\t\t\tdata-variant={variant === \"neon\" ? \"neon\" : undefined}\n\t\t\tdata-hdr={hdrOn ? \"\" : undefined}\n\t\t>\n\t\t\t{variant === \"neon\" && hdr ? (\n\t\t\t\t<canvas ref={hdrCanvasRef} className=\"tn-hdr\" aria-hidden=\"true\" />\n\t\t\t) : null}\n\t\t\t<ul ref={listRef} className=\"tn-list\">\n\t\t\t\t{items.map((item, i) => {\n\t\t\t\t\tconst isCurrent = item.id === value;\n\t\t\t\t\tconst inner = (\n\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t<svg\n\t\t\t\t\t\t\t\tclassName=\"tn-bar\"\n\t\t\t\t\t\t\t\tviewBox=\"0 0 54 8\"\n\t\t\t\t\t\t\t\tpreserveAspectRatio=\"none\"\n\t\t\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t<path className=\"tn-path\" d={TICK_NAV_FLAT_PATH} />\n\t\t\t\t\t\t\t\t{variant === \"neon\" ? (\n\t\t\t\t\t\t\t\t\t<path className=\"tn-beam\" d={TICK_NAV_FLAT_PATH} pathLength={100} />\n\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t</svg>\n\t\t\t\t\t\t\t<span className=\"tn-label\">\n\t\t\t\t\t\t\t\t{item.label}\n\t\t\t\t\t\t\t\t{item.badge ? <span className=\"tn-badge\">{item.badge}</span> : null}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</>\n\t\t\t\t\t);\n\t\t\t\t\treturn (\n\t\t\t\t\t\t<li\n\t\t\t\t\t\t\tkey={item.id}\n\t\t\t\t\t\t\tclassName=\"tn-item\"\n\t\t\t\t\t\t\tdata-current={isCurrent ? \"\" : undefined}\n\t\t\t\t\t\t\tdata-active={i === active ? \"\" : undefined}\n\t\t\t\t\t\t\tdata-disabled={item.disabled ? \"\" : undefined}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{item.href && !item.disabled ? (\n\t\t\t\t\t\t\t\t<a\n\t\t\t\t\t\t\t\t\tclassName=\"tn-link\"\n\t\t\t\t\t\t\t\t\thref={item.href}\n\t\t\t\t\t\t\t\t\taria-current={isCurrent ? \"page\" : undefined}\n\t\t\t\t\t\t\t\t\tonClick={(e) => activate(item, e)}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{inner}\n\t\t\t\t\t\t\t\t</a>\n\t\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\tclassName=\"tn-link\"\n\t\t\t\t\t\t\t\t\tdisabled={item.disabled}\n\t\t\t\t\t\t\t\t\taria-current={isCurrent ? \"page\" : undefined}\n\t\t\t\t\t\t\t\t\tonClick={(e) => activate(item, e)}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t{inner}\n\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t{ticks > 0 && i < items.length - 1 ? (\n\t\t\t\t\t\t\t\t<span className=\"tn-ticks\" aria-hidden=\"true\">\n\t\t\t\t\t\t\t\t\t{Array.from({ length: ticks }, (_, k) => (\n\t\t\t\t\t\t\t\t\t\t<span key={k} className=\"tn-tick\">\n\t\t\t\t\t\t\t\t\t\t\t<svg viewBox=\"0 0 54 8\" preserveAspectRatio=\"none\">\n\t\t\t\t\t\t\t\t\t\t\t\t<path className=\"tn-tick-path\" d={TICK_NAV_FLAT_PATH} />\n\t\t\t\t\t\t\t\t\t\t\t\t{variant === \"neon\" ? (\n\t\t\t\t\t\t\t\t\t\t\t\t\t<path className=\"tn-beam\" d={TICK_NAV_FLAT_PATH} pathLength={100} />\n\t\t\t\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t\t\t\t</svg>\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t))}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t</li>\n\t\t\t\t\t);\n\t\t\t\t})}\n\t\t\t</ul>\n\n\t\t\t{preview && activeItem ? (\n\t\t\t\t<div className=\"tn-preview\" aria-hidden=\"true\">\n\t\t\t\t\t{preview(activeItem)}\n\t\t\t\t</div>\n\t\t\t) : null}\n\t\t</nav>\n\t);\n}\n","type":"registry:ui","target":"@ui/fancy/components/tick-nav/TickNav.tsx"},{"path":"react/src/components/tick-nav/index.ts","content":"\"use client\";\n\nexport { TickNav } from \"./TickNav\";\nexport type { TickNavProps } from \"./TickNav\";\nexport type { TickNavItem } from \"./types\";\n","type":"registry:ui","target":"@ui/fancy/components/tick-nav/index.ts"},{"path":"react/src/components/tick-nav/tick-nav-core.ts","content":"\"use client\";\n\n/**\n * TickNav engine — framework-free, byte-identical in the Svelte, React and Vue\n * trees (see shared-cores.json).\n *\n * The component renders the markup; this module drives everything that moves:\n *\n * - a width spring per row (resting length → hover length),\n * - a proximity spring per row and per tick group, fed by a smoothstep of the\n *   distance, in rows, to the hovered row,\n * - a one-shot \"pluck\": a wave packet that runs along a line and leaves it\n *   flat again. Every line plucks — rows and ticks alike — the moment the\n *   pointer crosses it, so a sweep strums the ruler like strings,\n * - the preview card's position (it follows the pointer) and its fade-in.\n *\n * All output goes to CSS custom properties and to the pluck path's `d`, never\n * through framework state, so nothing re-renders per frame. The loop sleeps as\n * soon as every spring has settled.\n *\n * Expected markup, inside `list`:\n *   li.tn-item                     one per item, in order\n *     .tn-path                     the row's <path> (pluck target)\n *     .tn-ticks                    optional tick group after the row\n *       .tn-tick-path              one <path> per tick (pluck target)\n * A line's <svg> may also hold a `.tn-beam` path: it gets the same `d` as the\n * line during a pluck, and the <svg> gets `data-plucking` plus `--tn-p`, the\n * eased progress 0..1 of the wave's head, so CSS can ride a highlight on it.\n * and, inside `root`, an optional `.tn-preview` that reads `--tn-px`,\n * `--tn-py` and `--tn-po`.\n *\n * No browser global is touched at import time.\n */\n\nexport interface TickNavCoreOptions {\n\t/** Rows over which proximity falls to zero. */\n\treach: number;\n\t/** Resting line length, px. */\n\trestLength: number;\n\t/** Line length of the hovered and the current row, px. */\n\tactiveLength: number;\n\t/** Extra length, px, that proximity adds to lines and ticks. */\n\tproximityLength: number;\n\t/** Index of the current item, or -1. */\n\tcurrent: number;\n\t/** Snap instead of animating; no pluck. */\n\treducedMotion: boolean;\n\t/**\n\t * Called when the hovered (or keyboard-focused) row changes. `index` is -1\n\t * when nothing is hovered. `pointer` is true for a mouse or pen.\n\t */\n\tonActiveChange?: (index: number, pointer: boolean) => void;\n\t/**\n\t * Called each time a mouse or pen plucks a line: `kind` says whether it is\n\t * an item's line or a tick, `row` is the item it belongs to (a tick belongs\n\t * to the item above it). The place to play a sound.\n\t */\n\tonPluck?: (kind: \"row\" | \"tick\", row: number) => void;\n\t/**\n\t * Called after every painted frame with each line's state, for a renderer\n\t * that draws extra light on top (the HDR layer). Positions are CSS px\n\t * relative to `root`.\n\t */\n\tonFrame?: (lines: TickNavLineState[]) => void;\n}\n\nexport interface TickNavLineState {\n\tkind: \"row\" | \"tick\";\n\trow: number;\n\t/** Left end and vertical centre of the line, relative to `root`. */\n\tx: number;\n\ty: number;\n\t/** Current drawn length, px. */\n\tlength: number;\n\t/** Proximity 0..1. */\n\tnear: number;\n\t/** Eased progress of a running pluck's wave head, or null. */\n\tprogress: number | null;\n\tcurrent: boolean;\n}\n\nexport interface TickNavCore {\n\tupdate(options: Partial<TickNavCoreOptions>): void;\n\t/** Re-reads the rows after the item list changed. */\n\trefresh(): void;\n\t/** Re-measures line positions after a layout change (resize). */\n\tmeasure(): void;\n\tdestroy(): void;\n}\n\n/** Tick lengths; the component's CSS uses the same numbers. */\nexport const TICK_NAV_TICK_REST = 24;\nexport const TICK_NAV_TICK_GROW = 16;\n\nexport const TICK_NAV_DEFAULTS = Object.freeze({\n\treach: 4,\n\trestLength: 32,\n\tactiveLength: 55,\n\tproximityLength: 16,\n});\n\n/** The flat line the pluck starts from and returns to, in the 54×8 viewBox. */\nexport const TICK_NAV_FLAT_PATH = \"M0 4L54 4\";\n\nconst PLUCK_MS = 500;\n/** A row entered without crossing its line still plucks, unless it just did. */\nconst REPLUCK_MS = 120;\nconst RESTRIKE_MS = 60;\nconst PREVIEW_OFFSET = 14;\n\ninterface SpringConfig {\n\tstiffness: number;\n\tdamping: number;\n\tmass: number;\n}\n\nconst LENGTH_SPRING: SpringConfig = { stiffness: 250, damping: 30, mass: 1 };\nconst PROXIMITY_SPRING: SpringConfig = { stiffness: 400, damping: 30, mass: 0.6 };\nconst PREVIEW_SPRING: SpringConfig = { stiffness: 400, damping: 30, mass: 1 };\n\ninterface Spring {\n\tx: number;\n\tv: number;\n\ttarget: number;\n}\n\nfunction spring(x: number): Spring {\n\treturn { x, v: 0, target: x };\n}\n\n/** Semi-implicit Euler in fixed substeps; returns true while still moving. */\nfunction stepSpring(s: Spring, config: SpringConfig, dt: number, rest: number): boolean {\n\tconst h = 1 / 240;\n\tlet left = dt;\n\twhile (left > 0) {\n\t\tconst d = Math.min(h, left);\n\t\tconst a = (-config.stiffness * (s.x - s.target) - config.damping * s.v) / config.mass;\n\t\ts.v += a * d;\n\t\ts.x += s.v * d;\n\t\tleft -= d;\n\t}\n\tif (Math.abs(s.x - s.target) < rest && Math.abs(s.v) < rest * 10) {\n\t\ts.x = s.target;\n\t\ts.v = 0;\n\t\treturn false;\n\t}\n\treturn true;\n}\n\nfunction smoothstep(x: number): number {\n\tconst t = Math.min(1, Math.max(0, x));\n\treturn t * t * (3 - 2 * t);\n}\n\n/**\n * The pluck at progress `p` (0..1, already eased): a packet centred on\n * `54 * p`, 20 units wide, whose amplitude fades by 30% over the run.\n * `amplitude` scales the swing (ticks swing less than rows).\n */\nexport function pluckPath(p: number, amplitude = 1): string {\n\tconst head = 54 * p;\n\tconst fade = Math.max(0, 1 - (head / 54) * 0.3);\n\tlet d = \"\";\n\tfor (let x = 0; x <= 54; x += 1) {\n\t\tconst envelope = Math.max(0, 1 - Math.abs(head - x) / 20);\n\t\tconst y = 4 + 3.5 * amplitude * Math.sin((x - 0.5 * head) * 0.8) * envelope * fade;\n\t\td += `${x === 0 ? \"M\" : \"L\"}${x} ${y.toFixed(2)}`;\n\t}\n\treturn d;\n}\n\ninterface Line {\n\tpath: SVGPathElement;\n\tbeam: SVGPathElement | null;\n\tsvg: SVGSVGElement | null;\n\tkind: \"row\" | \"tick\";\n\trow: number;\n\t/** Centre, list-local px; read once per pointer visit. */\n\ty: number;\n\t/** Left end and centre relative to `root`, for `onFrame`. */\n\trootX: number;\n\trootY: number;\n\tprogress: number | null;\n\tpluckStart: number | null;\n\tlastPluck: number;\n}\n\ninterface Row {\n\tel: HTMLElement;\n\tline: Line | null;\n\tlength: Spring;\n\tnear: Spring;\n\ttickNear: Spring;\n}\n\nconst TICK_AMPLITUDE = 0.7;\n\nexport function createTickNavCore(\n\troot: HTMLElement,\n\tlist: HTMLElement,\n\tinitial: Partial<TickNavCoreOptions> = {}\n): TickNavCore {\n\tconst options: TickNavCoreOptions = {\n\t\treach: TICK_NAV_DEFAULTS.reach,\n\t\trestLength: TICK_NAV_DEFAULTS.restLength,\n\t\tactiveLength: TICK_NAV_DEFAULTS.activeLength,\n\t\tproximityLength: TICK_NAV_DEFAULTS.proximityLength,\n\t\tcurrent: -1,\n\t\treducedMotion: false,\n\t\t...initial,\n\t};\n\n\tlet rows: Row[] = [];\n\tlet lines: Line[] = [];\n\tlet hovered = -1;\n\tlet lastY: number | null = null;\n\tconst preview = spring(0);\n\tlet frame = 0;\n\tlet lastT = 0;\n\tlet destroyed = false;\n\n\tfunction readRows(): void {\n\t\tconst previous = rows;\n\t\t// Keep each surviving line's state (a running pluck above all): a refresh\n\t\t// mid-pluck must not orphan a wave on its path.\n\t\tconst known = new Map(lines.map((line) => [line.path, line]));\n\t\tlines = [];\n\t\trows = Array.from(list.querySelectorAll<HTMLElement>(\":scope > .tn-item\")).map((el, i) => {\n\t\t\tconst old = previous[i];\n\t\t\tconst rest = i === options.current ? options.activeLength : options.restLength;\n\t\t\tconst path = el.querySelector<SVGPathElement>(\".tn-path\");\n\t\t\tconst line: Line | null = path ? reuse(known, path, \"row\", i) : null;\n\t\t\tif (line) lines.push(line);\n\t\t\tel.querySelectorAll<SVGPathElement>(\".tn-tick-path\").forEach((tick) => {\n\t\t\t\tlines.push(reuse(known, tick, \"tick\", i));\n\t\t\t});\n\t\t\treturn {\n\t\t\t\tel,\n\t\t\t\tline,\n\t\t\t\tlength: old?.length ?? spring(rest),\n\t\t\t\tnear: old?.near ?? spring(0),\n\t\t\t\ttickNear: old?.tickNear ?? spring(0),\n\t\t\t};\n\t\t});\n\t}\n\n\tfunction reuse(\n\t\tknown: Map<SVGPathElement, Line>,\n\t\tpath: SVGPathElement,\n\t\tkind: \"row\" | \"tick\",\n\t\trow: number\n\t): Line {\n\t\tconst line = known.get(path);\n\t\tif (!line) return makeLine(path, kind, row);\n\t\tline.kind = kind;\n\t\tline.row = row;\n\t\tline.beam = line.svg?.querySelector<SVGPathElement>(\".tn-beam\") ?? null;\n\t\treturn line;\n\t}\n\n\tfunction makeLine(path: SVGPathElement, kind: \"row\" | \"tick\", row: number): Line {\n\t\tconst svg = path.ownerSVGElement;\n\t\treturn {\n\t\t\tpath,\n\t\t\tbeam: svg?.querySelector<SVGPathElement>(\".tn-beam\") ?? null,\n\t\t\tsvg,\n\t\t\tkind,\n\t\t\trow,\n\t\t\ty: 0,\n\t\t\trootX: 0,\n\t\t\trootY: 0,\n\t\t\tprogress: null,\n\t\t\tpluckStart: null,\n\t\t\tlastPluck: -Infinity,\n\t\t};\n\t}\n\n\tfunction setShape(line: Line, d: string, progress: number | null): void {\n\t\tline.progress = progress;\n\t\tline.path.setAttribute(\"d\", d);\n\t\tline.beam?.setAttribute(\"d\", d);\n\t\tif (!line.svg) return;\n\t\tif (progress === null) {\n\t\t\tline.svg.removeAttribute(\"data-plucking\");\n\t\t\tline.svg.style.removeProperty(\"--tn-p\");\n\t\t} else {\n\t\t\tline.svg.setAttribute(\"data-plucking\", \"\");\n\t\t\tline.svg.style.setProperty(\"--tn-p\", progress.toFixed(4));\n\t\t}\n\t}\n\n\tfunction measureLines(): void {\n\t\tconst top = list.getBoundingClientRect().top;\n\t\tconst rootBox = root.getBoundingClientRect();\n\t\tfor (const line of lines) {\n\t\t\tconst box = line.path.getBoundingClientRect();\n\t\t\tconst centre = box.top + box.height / 2;\n\t\t\tline.y = centre - top;\n\t\t\tline.rootX = (line.svg ?? line.path).getBoundingClientRect().left - rootBox.left;\n\t\t\tline.rootY = centre - rootBox.top;\n\t\t}\n\t}\n\n\tfunction retarget(): void {\n\t\tconst reach = Math.max(0.5, options.reach);\n\t\trows.forEach((row, i) => {\n\t\t\trow.length.target =\n\t\t\t\ti === hovered || i === options.current ? options.activeLength : options.restLength;\n\t\t\tconst off = hovered < 0 || options.reducedMotion;\n\t\t\trow.near.target = off ? 0 : smoothstep(1 - Math.abs(i - hovered) / reach);\n\t\t\trow.tickNear.target = off ? 0 : smoothstep(1 - Math.abs(i + 0.5 - hovered) / reach);\n\t\t});\n\t\tpreview.target = hovered >= 0 ? 1 : 0;\n\t\tif (hovered < 0) {\n\t\t\t// The card unmounts with the hover; the next one fades in from 0.\n\t\t\tpreview.x = 0;\n\t\t\tpreview.v = 0;\n\t\t}\n\t}\n\n\tfunction paint(): void {\n\t\tfor (const row of rows) {\n\t\t\tconst width = row.length.x + options.proximityLength * row.near.x;\n\t\t\trow.el.style.setProperty(\"--tn-len\", `${width.toFixed(2)}px`);\n\t\t\trow.el.style.setProperty(\"--tn-near\", row.near.x.toFixed(4));\n\t\t\trow.el.style.setProperty(\"--tn-tick-near\", row.tickNear.x.toFixed(4));\n\t\t}\n\t\troot.style.setProperty(\"--tn-po\", preview.x.toFixed(4));\n\t\tif (options.onFrame) {\n\t\t\t// An extra renderer must never stop the motion: drop it on failure.\n\t\t\ttry {\n\t\t\t\toptions.onFrame(frameState());\n\t\t\t} catch (error) {\n\t\t\t\toptions.onFrame = undefined;\n\t\t\t\tconsole.warn(\"[TickNav] light layer disabled:\", error);\n\t\t\t}\n\t\t}\n\t}\n\n\tfunction frameState(): TickNavLineState[] {\n\t\treturn lines.map((line) => {\n\t\t\tconst row = rows[line.row];\n\t\t\tconst near = line.kind === \"row\" ? (row?.near.x ?? 0) : (row?.tickNear.x ?? 0);\n\t\t\tconst length =\n\t\t\t\tline.kind === \"row\"\n\t\t\t\t\t? (row?.length.x ?? 0) + options.proximityLength * near\n\t\t\t\t\t: TICK_NAV_TICK_REST + TICK_NAV_TICK_GROW * near;\n\t\t\treturn {\n\t\t\t\tkind: line.kind,\n\t\t\t\trow: line.row,\n\t\t\t\tx: line.rootX,\n\t\t\t\ty: line.rootY,\n\t\t\t\tlength,\n\t\t\t\tnear,\n\t\t\t\tprogress: line.progress,\n\t\t\t\tcurrent: line.kind === \"row\" && line.row === options.current,\n\t\t\t};\n\t\t});\n\t}\n\n\tfunction snapAll(): void {\n\t\tfor (const row of rows) {\n\t\t\tfor (const s of [row.length, row.near, row.tickNear]) {\n\t\t\t\ts.x = s.target;\n\t\t\t\ts.v = 0;\n\t\t\t}\n\t\t}\n\t\tpreview.x = preview.target;\n\t\tpreview.v = 0;\n\t}\n\n\tfunction step(): void {\n\t\tframe = 0;\n\t\tif (destroyed) return;\n\t\t// One clock for springs and plucks: pluck start times come from\n\t\t// performance.now(), which can run ahead of the rAF timestamp.\n\t\tconst t = performance.now();\n\t\tconst dt = lastT ? Math.min(1 / 30, (t - lastT) / 1000) : 1 / 60;\n\t\tlastT = t;\n\t\tlet moving = false;\n\t\tfor (const row of rows) {\n\t\t\tif (stepSpring(row.length, LENGTH_SPRING, dt, 0.05)) moving = true;\n\t\t\tif (stepSpring(row.near, PROXIMITY_SPRING, dt, 0.001)) moving = true;\n\t\t\tif (stepSpring(row.tickNear, PROXIMITY_SPRING, dt, 0.001)) moving = true;\n\t\t}\n\t\tfor (const line of lines) {\n\t\t\tif (line.pluckStart === null) continue;\n\t\t\tconst p = Math.min((t - line.pluckStart) / PLUCK_MS, 1);\n\t\t\tif (p < 1) {\n\t\t\t\tconst amplitude = line.kind === \"tick\" ? TICK_AMPLITUDE : 1;\n\t\t\t\tconst eased = 1 - Math.pow(1 - p, 3);\n\t\t\t\tsetShape(line, pluckPath(eased, amplitude), eased);\n\t\t\t\tmoving = true;\n\t\t\t} else {\n\t\t\t\tsetShape(line, TICK_NAV_FLAT_PATH, null);\n\t\t\t\tline.pluckStart = null;\n\t\t\t}\n\t\t}\n\t\tif (stepSpring(preview, PREVIEW_SPRING, dt, 0.001)) moving = true;\n\t\tpaint();\n\t\tif (moving) frame = requestAnimationFrame(step);\n\t\telse lastT = 0;\n\t}\n\n\tfunction kick(): void {\n\t\tif (options.reducedMotion) {\n\t\t\tsnapAll();\n\t\t\tpaint();\n\t\t\treturn;\n\t\t}\n\t\tif (!frame) frame = requestAnimationFrame(step);\n\t}\n\n\tfunction stopPluck(line: Line): void {\n\t\tline.pluckStart = null;\n\t\tsetShape(line, TICK_NAV_FLAT_PATH, null);\n\t}\n\n\t/** Restarts the line's pluck (a new strike interrupts a running one). */\n\tfunction pluck(line: Line, pointer: boolean, now: number): void {\n\t\tline.lastPluck = now;\n\t\tif (!options.reducedMotion) {\n\t\t\tline.pluckStart = now;\n\t\t\tkick();\n\t\t}\n\t\tif (pointer && !isDisabled(line.row)) options.onPluck?.(line.kind, line.row);\n\t}\n\n\t/**\n\t * `pluckRow`: a keyboard focus, or a pointer arriving from outside the list,\n\t * crosses no line, so the row plucks on entry. Otherwise the crossing does it\n\t * (the hit area is entered a few px before the line is reached).\n\t */\n\tfunction setHovered(index: number, pointer: boolean, pluckRow = !pointer): void {\n\t\tif (index === hovered) return;\n\t\thovered = index;\n\t\tconst line = rows[index]?.line;\n\t\tconst now = performance.now();\n\t\tif (line && pluckRow && now - line.lastPluck > REPLUCK_MS) pluck(line, pointer, now);\n\t\tretarget();\n\t\tkick();\n\t\toptions.onActiveChange?.(index, pointer);\n\t}\n\n\tfunction rowIndexOf(target: EventTarget | null): number {\n\t\tconst el = target instanceof Element ? target.closest(\".tn-item\") : null;\n\t\treturn el ? rows.findIndex((r) => r.el === el) : -1;\n\t}\n\n\tfunction isDisabled(index: number): boolean {\n\t\treturn rows[index]?.el.hasAttribute(\"data-disabled\") ?? false;\n\t}\n\n\t/** The row holding visible keyboard focus, or -1. */\n\tfunction focusedIndex(): number {\n\t\tconst el = list.ownerDocument.activeElement;\n\t\tif (!el || !list.contains(el) || !el.matches(\":focus-visible\")) return -1;\n\t\tconst index = rowIndexOf(el);\n\t\treturn isDisabled(index) ? -1 : index;\n\t}\n\n\t/** Anchors the card beside a row, for when there is no pointer to follow. */\n\tfunction anchorTo(index: number): void {\n\t\tconst row = rows[index];\n\t\tif (!row) return;\n\t\tconst box = root.getBoundingClientRect();\n\t\tconst rowBox = row.el.getBoundingClientRect();\n\t\troot.style.setProperty(\"--tn-px\", `${(rowBox.right - box.left + PREVIEW_OFFSET).toFixed(1)}px`);\n\t\troot.style.setProperty(\"--tn-py\", `${(rowBox.top - box.top + PREVIEW_OFFSET).toFixed(1)}px`);\n\t}\n\n\t/**\n\t * The pointer no longer designates a row: hand the active row back to the\n\t * keyboard-focused one if there is one, otherwise clear it. No pluck.\n\t */\n\tfunction releasePointer(): void {\n\t\tconst focused = focusedIndex();\n\t\tif (focused >= 0) anchorTo(focused);\n\t\tsetHovered(focused, false, false);\n\t}\n\n\t// Hover is per row (the link's hit area), like `mouseenter`: entering a row\n\t// activates it, leaving the list releases it. Moving between rows never\n\t// passes through \"nothing hovered\", so the card does not flicker.\n\tfunction onPointerOver(event: PointerEvent): void {\n\t\tif (event.pointerType === \"touch\") return;\n\t\tconst link = event.target instanceof Element ? event.target.closest(\".tn-link\") : null;\n\t\tif (!link) return;\n\t\tconst index = rowIndexOf(link);\n\t\tif (index < 0) return;\n\t\t// A disabled row never activates, and must not leave the previous row lit.\n\t\tif (isDisabled(index)) {\n\t\t\tif (hovered >= 0) releasePointer();\n\t\t\treturn;\n\t\t}\n\t\tsetHovered(index, true, lastY === null);\n\t}\n\n\t// Every line whose centre lies between the previous and the current pointer\n\t// position was crossed: pluck each one, in crossing order.\n\tfunction strum(event: PointerEvent): void {\n\t\tconst y = event.clientY - list.getBoundingClientRect().top;\n\t\tif (lastY === null) {\n\t\t\tmeasureLines();\n\t\t\tlastY = y;\n\t\t\treturn;\n\t\t}\n\t\tconst from = lastY;\n\t\tlastY = y;\n\t\tif (from === y) return;\n\t\tconst down = y > from;\n\t\tconst lo = Math.min(from, y);\n\t\tconst hi = Math.max(from, y);\n\t\tconst crossed = lines.filter((line) => line.y > lo && line.y <= hi);\n\t\tcrossed.sort((a, b) => (down ? a.y - b.y : b.y - a.y));\n\t\tconst now = performance.now();\n\t\tfor (const line of crossed) {\n\t\t\t// The same line struck twice within a frame or two is one strike.\n\t\t\tif (now - line.lastPluck < RESTRIKE_MS) continue;\n\t\t\tpluck(line, true, now);\n\t\t}\n\t}\n\n\tfunction onPointerMove(event: PointerEvent): void {\n\t\tif (event.pointerType === \"touch\") return;\n\t\tif (list.contains(event.target as Node)) strum(event);\n\t\tconst box = root.getBoundingClientRect();\n\t\troot.style.setProperty(\n\t\t\t\"--tn-px\",\n\t\t\t`${(event.clientX - box.left + PREVIEW_OFFSET).toFixed(1)}px`\n\t\t);\n\t\troot.style.setProperty(\"--tn-py\", `${(event.clientY - box.top + PREVIEW_OFFSET).toFixed(1)}px`);\n\t}\n\n\tfunction onPointerLeave(event: PointerEvent): void {\n\t\tif (event.pointerType === \"touch\") return;\n\t\tlastY = null;\n\t\treleasePointer();\n\t}\n\n\tfunction onFocusIn(event: FocusEvent): void {\n\t\tconst el = event.target as Element | null;\n\t\tif (!el || !(el as HTMLElement).matches?.(\":focus-visible\")) return;\n\t\tconst index = rowIndexOf(el);\n\t\tif (!rows[index]) return;\n\t\t// Anchor the card beside the focused row, since there is no pointer.\n\t\tanchorTo(index);\n\t\tsetHovered(index, false);\n\t}\n\n\tfunction onFocusOut(event: FocusEvent): void {\n\t\tconst next = event.relatedTarget as Node | null;\n\t\tif (next && list.contains(next)) return;\n\t\tsetHovered(-1, false);\n\t}\n\n\treadRows();\n\tmeasureLines();\n\tretarget();\n\tsnapAll();\n\tpaint();\n\n\tlist.addEventListener(\"pointerover\", onPointerOver);\n\troot.addEventListener(\"pointermove\", onPointerMove);\n\tlist.addEventListener(\"pointerleave\", onPointerLeave);\n\tlist.addEventListener(\"focusin\", onFocusIn);\n\tlist.addEventListener(\"focusout\", onFocusOut);\n\n\treturn {\n\t\tupdate(next) {\n\t\t\tconst reducedBefore = options.reducedMotion;\n\t\t\tObject.assign(options, next);\n\t\t\tif (options.reducedMotion && !reducedBefore) {\n\t\t\t\tfor (const line of lines) stopPluck(line);\n\t\t\t}\n\t\t\tretarget();\n\t\t\tkick();\n\t\t},\n\t\trefresh() {\n\t\t\treadRows();\n\t\t\tmeasureLines();\n\t\t\tlastY = null;\n\t\t\tif (hovered >= rows.length) hovered = -1;\n\t\t\tretarget();\n\t\t\tkick();\n\t\t\tpaint();\n\t\t},\n\t\tmeasure() {\n\t\t\t// Width changes as lines grow; the strum's last position stays valid.\n\t\t\tmeasureLines();\n\t\t\tif (options.onFrame) paint();\n\t\t},\n\t\tdestroy() {\n\t\t\tdestroyed = true;\n\t\t\tif (frame) cancelAnimationFrame(frame);\n\t\t\tframe = 0;\n\t\t\tlist.removeEventListener(\"pointerover\", onPointerOver);\n\t\t\troot.removeEventListener(\"pointermove\", onPointerMove);\n\t\t\tlist.removeEventListener(\"pointerleave\", onPointerLeave);\n\t\t\tlist.removeEventListener(\"focusin\", onFocusIn);\n\t\t\tlist.removeEventListener(\"focusout\", onFocusOut);\n\t\t},\n\t};\n}\n","type":"registry:ui","target":"@ui/fancy/components/tick-nav/tick-nav-core.ts"},{"path":"react/src/components/tick-nav/tick-nav-hdr.ts","content":"\"use client\";\n\n/**\n * TickNav HDR light — framework-free, byte-identical in the Svelte, React and\n * Vue trees (see shared-cores.json).\n *\n * CSS and SVG colours are clamped to SDR white, so a DOM glow can never be\n * brighter than the page's white. This module draws the neon's light into a\n * WebGPU canvas configured with `rgba16float` + `toneMapping: { mode:\n * \"extended\" }`: on an HDR display, values above 1.0 render brighter than SDR\n * white. 1.0 is the SDR reference white (~203 nits in BT.2408 terms), so the\n * beam's peak of ~4.9 lands around 1000 nits on a display with the headroom.\n *\n * It only starts when the browser keeps extended tone mapping AND the display\n * reports `(dynamic-range: high)`; otherwise `startTickNavHdr` resolves to\n * null and the CSS glow carries the effect alone. No browser global is touched\n * at import time.\n */\n\nimport type { TickNavLineState } from \"./tick-nav-core\";\n\n/** One glowing segment, in CSS px relative to the canvas's top-left corner. */\nexport interface TickNavLight {\n\tx0: number;\n\tx1: number;\n\ty: number;\n\t/** Tube radius, px (Gaussian core). */\n\tradius: number;\n\t/** Halo falloff, px (exponential skirt). */\n\thalo: number;\n\t/** Linear-light multiplier on the accent: 1 = SDR white level. */\n\tintensity: number;\n\t/** Mix toward white for the hot core of the light, 0..1. */\n\twhite: number;\n}\n\nexport interface TickNavHdr {\n\t/** Replaces everything drawn with these lights. */\n\trender(lights: TickNavLight[]): void;\n\t/** Canvas size in CSS px (the backing store follows devicePixelRatio). */\n\tresize(width: number, height: number): void;\n\t/** Accent colour as 8-bit sRGB. */\n\tsetAccent(rgb: [number, number, number]): void;\n\tdestroy(): void;\n}\n\n/** SDR white ≈ 203 nits; 1000 nits / 203 ≈ 4.9. */\nexport const TICK_NAV_HDR_PEAK = 4.9;\n\nconst MAX_LIGHTS = 256;\nconst MAX_DPR = 2;\nconst FORMAT = \"rgba16float\" as GPUTextureFormat;\n\nconst WGSL = /* wgsl */ `\nstruct Frame {\n\tsize: vec2f,\n\taccent: vec3f,\n};\n@group(0) @binding(0) var<uniform> frame: Frame;\n\nstruct Light {\n\t@location(0) seg: vec4f,   // x0, y, x1, radius\n\t@location(1) look: vec4f,  // halo, intensity, white, unused\n};\n\nstruct Out {\n\t@builtin(position) pos: vec4f,\n\t@location(0) p: vec2f,\n\t@location(1) seg: vec4f,\n\t@location(2) look: vec4f,\n};\n\n@vertex\nfn vs(@builtin(vertex_index) vi: u32, light: Light) -> Out {\n\tlet corners = array<vec2f, 6>(\n\t\tvec2f(0.0, 0.0), vec2f(1.0, 0.0), vec2f(0.0, 1.0),\n\t\tvec2f(0.0, 1.0), vec2f(1.0, 0.0), vec2f(1.0, 1.0)\n\t);\n\tlet reach = light.seg.w * 2.0 + light.look.x * 6.0;\n\tlet lo = vec2f(min(light.seg.x, light.seg.z) - reach, light.seg.y - reach);\n\tlet hi = vec2f(max(light.seg.x, light.seg.z) + reach, light.seg.y + reach);\n\tlet p = mix(lo, hi, corners[vi]);\n\tvar out: Out;\n\tout.pos = vec4f(p.x / frame.size.x * 2.0 - 1.0, 1.0 - p.y / frame.size.y * 2.0, 0.0, 1.0);\n\tout.p = p;\n\tout.seg = light.seg;\n\tout.look = light.look;\n\treturn out;\n}\n\n@fragment\nfn fs(in: Out) -> @location(0) vec4f {\n\tlet a = vec2f(in.seg.x, in.seg.y);\n\tlet b = vec2f(in.seg.z, in.seg.y);\n\tlet ab = b - a;\n\tlet t = clamp(dot(in.p - a, ab) / max(dot(ab, ab), 1e-4), 0.0, 1.0);\n\tlet d = length(in.p - (a + ab * t));\n\tlet core = exp(-(d * d) / max(in.seg.w * in.seg.w, 1e-3));\n\tlet halo = exp(-d / max(in.look.x, 1e-3)) * 0.35;\n\tlet v = (core + halo) * in.look.y;\n\tlet colour = mix(frame.accent, vec3f(1.0), in.look.z * core);\n\t// Premultiplied, nearly additive: light adds to the page under it.\n\treturn vec4f(colour * v, clamp(v * 0.08, 0.0, 1.0));\n}\n`;\n\nfunction srgbToLinear(c: number): number {\n\tconst s = c / 255;\n\treturn s <= 0.04045 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);\n}\n\nexport async function startTickNavHdr(canvas: HTMLCanvasElement): Promise<TickNavHdr | null> {\n\ttry {\n\t\tif (typeof navigator === \"undefined\" || typeof window === \"undefined\") return null;\n\t\tconst hdrDisplay =\n\t\t\ttypeof window.matchMedia === \"function\" && window.matchMedia(\"(dynamic-range: high)\").matches;\n\t\tif (!hdrDisplay) return null;\n\t\tconst gpu = (navigator as Navigator & { gpu?: GPU }).gpu;\n\t\tif (!gpu) return null;\n\t\tconst adapter = await gpu.requestAdapter();\n\t\tif (!adapter) return null;\n\t\tconst device = await adapter.requestDevice();\n\t\tconst context = canvas.getContext(\"webgpu\");\n\t\tif (!context) {\n\t\t\tdevice.destroy();\n\t\t\treturn null;\n\t\t}\n\t\tcontext.configure({\n\t\t\tdevice,\n\t\t\tformat: FORMAT,\n\t\t\talphaMode: \"premultiplied\",\n\t\t\tcolorSpace: \"display-p3\",\n\t\t\ttoneMapping: { mode: \"extended\" },\n\t\t} as GPUCanvasConfiguration);\n\t\tlet extended = false;\n\t\ttry {\n\t\t\tconst applied = context.getConfiguration?.() as\n\t\t\t\t| (GPUCanvasConfiguration & { toneMapping?: { mode?: string } })\n\t\t\t\t| null;\n\t\t\textended = applied?.toneMapping?.mode === \"extended\";\n\t\t} catch {\n\t\t\textended = false;\n\t\t}\n\t\tif (!extended) {\n\t\t\tcontext.unconfigure();\n\t\t\tdevice.destroy();\n\t\t\treturn null;\n\t\t}\n\n\t\tconst module = device.createShaderModule({ code: WGSL });\n\t\tconst uniform = device.createBuffer({\n\t\t\tsize: 32,\n\t\t\tusage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,\n\t\t});\n\t\tconst instances = device.createBuffer({\n\t\t\tsize: MAX_LIGHTS * 8 * 4,\n\t\t\tusage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,\n\t\t});\n\t\tconst pipeline = device.createRenderPipeline({\n\t\t\tlayout: \"auto\",\n\t\t\tvertex: {\n\t\t\t\tmodule,\n\t\t\t\tentryPoint: \"vs\",\n\t\t\t\tbuffers: [\n\t\t\t\t\t{\n\t\t\t\t\t\tarrayStride: 32,\n\t\t\t\t\t\tstepMode: \"instance\",\n\t\t\t\t\t\tattributes: [\n\t\t\t\t\t\t\t{ shaderLocation: 0, offset: 0, format: \"float32x4\" },\n\t\t\t\t\t\t\t{ shaderLocation: 1, offset: 16, format: \"float32x4\" },\n\t\t\t\t\t\t],\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t},\n\t\t\tfragment: {\n\t\t\t\tmodule,\n\t\t\t\tentryPoint: \"fs\",\n\t\t\t\ttargets: [\n\t\t\t\t\t{\n\t\t\t\t\t\tformat: FORMAT,\n\t\t\t\t\t\tblend: {\n\t\t\t\t\t\t\tcolor: { srcFactor: \"one\", dstFactor: \"one\", operation: \"add\" },\n\t\t\t\t\t\t\talpha: { srcFactor: \"one\", dstFactor: \"one\", operation: \"add\" },\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t},\n\t\t\tprimitive: { topology: \"triangle-list\" },\n\t\t});\n\t\tconst bindGroup = device.createBindGroup({\n\t\t\tlayout: pipeline.getBindGroupLayout(0),\n\t\t\tentries: [{ binding: 0, resource: { buffer: uniform } }],\n\t\t});\n\n\t\tlet width = 1;\n\t\tlet height = 1;\n\t\tlet accent: [number, number, number] = [0.6, 0.3, 1];\n\t\tlet destroyed = false;\n\t\tconst data = new Float32Array(MAX_LIGHTS * 8);\n\n\t\tfunction writeUniform(): void {\n\t\t\tdevice.queue.writeBuffer(\n\t\t\t\tuniform,\n\t\t\t\t0,\n\t\t\t\tnew Float32Array([width, height, 0, 0, accent[0], accent[1], accent[2], 0])\n\t\t\t);\n\t\t}\n\t\twriteUniform();\n\n\t\treturn {\n\t\t\trender(lights) {\n\t\t\t\tif (destroyed) return;\n\t\t\t\tconst n = Math.min(lights.length, MAX_LIGHTS);\n\t\t\t\tfor (let i = 0; i < n; i++) {\n\t\t\t\t\tconst l = lights[i];\n\t\t\t\t\tif (!l) continue;\n\t\t\t\t\tdata.set([l.x0, l.y, l.x1, l.radius, l.halo, l.intensity, l.white, 0], i * 8);\n\t\t\t\t}\n\t\t\t\tif (n) device.queue.writeBuffer(instances, 0, data, 0, n * 8);\n\t\t\t\tconst encoder = device.createCommandEncoder();\n\t\t\t\tconst pass = encoder.beginRenderPass({\n\t\t\t\t\tcolorAttachments: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tview: context.getCurrentTexture().createView(),\n\t\t\t\t\t\t\tclearValue: { r: 0, g: 0, b: 0, a: 0 },\n\t\t\t\t\t\t\tloadOp: \"clear\",\n\t\t\t\t\t\t\tstoreOp: \"store\",\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t});\n\t\t\t\tif (n) {\n\t\t\t\t\tpass.setPipeline(pipeline);\n\t\t\t\t\tpass.setBindGroup(0, bindGroup);\n\t\t\t\t\tpass.setVertexBuffer(0, instances);\n\t\t\t\t\tpass.draw(6, n);\n\t\t\t\t}\n\t\t\t\tpass.end();\n\t\t\t\tdevice.queue.submit([encoder.finish()]);\n\t\t\t},\n\t\t\tresize(w, h) {\n\t\t\t\twidth = Math.max(1, w);\n\t\t\t\theight = Math.max(1, h);\n\t\t\t\tconst dpr = Math.min(window.devicePixelRatio || 1, MAX_DPR);\n\t\t\t\tcanvas.width = Math.round(width * dpr);\n\t\t\t\tcanvas.height = Math.round(height * dpr);\n\t\t\t\twriteUniform();\n\t\t\t},\n\t\t\tsetAccent(rgb) {\n\t\t\t\taccent = [srgbToLinear(rgb[0]), srgbToLinear(rgb[1]), srgbToLinear(rgb[2])];\n\t\t\t\twriteUniform();\n\t\t\t},\n\t\t\tdestroy() {\n\t\t\t\tdestroyed = true;\n\t\t\t\ttry {\n\t\t\t\t\tcontext.unconfigure();\n\t\t\t\t} catch {\n\t\t\t\t\t// already gone\n\t\t\t\t}\n\t\t\t\tdevice.destroy();\n\t\t\t},\n\t\t};\n\t} catch {\n\t\treturn null;\n\t}\n}\n\n/** Margin, px, the HDR canvas extends past the nav on every side for the halo. */\nexport const TICK_NAV_HDR_PAD = 24;\n\n/**\n * Turns the core's per-line state into lights: a tube along every lit line\n * (proximity, or the current row) and a hot spark on every running pluck's\n * wave head, peaking at {@link TICK_NAV_HDR_PEAK}.\n */\nexport function tickNavLights(lines: TickNavLineState[]): TickNavLight[] {\n\tconst out: TickNavLight[] = [];\n\tconst pad = TICK_NAV_HDR_PAD;\n\tfor (const line of lines) {\n\t\tconst row = line.kind === \"row\";\n\t\t// Tubes sit around SDR white (1.0) at most; the headroom above is kept\n\t\t// for the sparks, so the peak reads as a flash, not a white-hot rail.\n\t\tconst tube = Math.max(line.current ? 0.9 : 0, line.near * (row ? 1.1 : 0.45));\n\t\tif (tube > 0.02) {\n\t\t\tout.push({\n\t\t\t\tx0: line.x + pad,\n\t\t\t\tx1: line.x + line.length + pad,\n\t\t\t\ty: line.y + pad,\n\t\t\t\tradius: row ? 1.1 : 0.8,\n\t\t\t\thalo: 3 + line.near * 5,\n\t\t\t\tintensity: tube,\n\t\t\t\twhite: 0.2,\n\t\t\t});\n\t\t}\n\t\tif (line.progress !== null) {\n\t\t\tconst head = line.x + line.length * line.progress + pad;\n\t\t\tout.push({\n\t\t\t\tx0: head - 3,\n\t\t\t\tx1: head + 3,\n\t\t\t\ty: line.y + pad,\n\t\t\t\tradius: row ? 1.6 : 1.2,\n\t\t\t\thalo: 7,\n\t\t\t\tintensity: TICK_NAV_HDR_PEAK * Math.pow(1 - line.progress, 0.6) * (row ? 1 : 0.6),\n\t\t\t\twhite: 0.85,\n\t\t\t});\n\t\t}\n\t}\n\treturn out;\n}\n\n/**\n * Calls `onChange` with `el`'s computed `color` now, then again whenever it\n * may have changed with the page's theme: a `class`, `style` or `data-theme`\n * change on `<html>` or `<body>`, or a flip of the colour-scheme preference.\n * Unchanged values are not reported. Returns a function that stops watching.\n */\nexport function watchTickNavAccent(el: Element, onChange: (css: string) => void): () => void {\n\tconst view = el.ownerDocument.defaultView;\n\tif (!view) return () => {};\n\tlet last = \"\";\n\tconst read = () => {\n\t\tconst css = view.getComputedStyle(el).color;\n\t\tif (css === last) return;\n\t\tlast = css;\n\t\tonChange(css);\n\t};\n\tread();\n\tconst doc = el.ownerDocument;\n\tconst observer = new view.MutationObserver(read);\n\tconst watched = { attributes: true, attributeFilter: [\"class\", \"style\", \"data-theme\"] };\n\tobserver.observe(doc.documentElement, watched);\n\tif (doc.body) observer.observe(doc.body, watched);\n\tconst scheme = view.matchMedia?.(\"(prefers-color-scheme: dark)\");\n\tscheme?.addEventListener?.(\"change\", read);\n\treturn () => {\n\t\tobserver.disconnect();\n\t\tscheme?.removeEventListener?.(\"change\", read);\n\t};\n}\n\n/** Resolves any CSS colour the browser understands to 8-bit sRGB. */\nexport function resolveCssColor(css: string): [number, number, number] | null {\n\tif (typeof document === \"undefined\") return null;\n\tconst ctx = document.createElement(\"canvas\").getContext(\"2d\", { willReadFrequently: true });\n\tif (!ctx) return null;\n\tctx.fillStyle = \"#000\";\n\tctx.fillStyle = css;\n\tctx.fillRect(0, 0, 1, 1);\n\tconst px = ctx.getImageData(0, 0, 1, 1).data;\n\treturn [px[0] ?? 0, px[1] ?? 0, px[2] ?? 0];\n}\n","type":"registry:ui","target":"@ui/fancy/components/tick-nav/tick-nav-hdr.ts"},{"path":"react/src/components/tick-nav/tick-nav.css","content":".tn-root {\n\t--tn-accent: var(\n\t\t--ft-accent,\n\t\tlight-dark(oklch(0.5432 0.2528 300.22), oklch(0.604 0.2606 301.75))\n\t);\n\t--tn-border: color-mix(in oklab, currentColor 16%, transparent);\n\t--tn-row-gap: 0.5rem;\n\tposition: relative;\n}\n\n.tn-list,\n.tn-item {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: var(--tn-row-gap);\n\tmargin: 0;\n\tpadding: 0;\n\tlist-style: none;\n}\n\n/* A row is one pixel tall, like the ruler line it sits on; the ::after\n   below gives it a comfortable hit area that overlaps the ticks around it. */\n.tn-link {\n\tposition: relative;\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 0.75rem;\n\theight: 1px;\n\tmargin: 0;\n\tpadding: 0;\n\tborder: 0;\n\tbackground: none;\n\tfont: inherit;\n\tcolor: inherit;\n\ttext-align: left;\n\ttext-decoration: none;\n\tcursor: pointer;\n\toutline: none;\n}\n\n.tn-link::after {\n\tcontent: \"\";\n\tposition: absolute;\n\tleft: 0;\n\ttop: 50%;\n\twidth: 100%;\n\theight: 1.75rem;\n\ttransform: translateY(-50%);\n}\n\n.tn-bar {\n\tflex-shrink: 0;\n\twidth: var(--tn-len, 32px);\n\theight: 0.5rem;\n\toverflow: visible;\n}\n\n.tn-path {\n\tfill: none;\n\tstroke: color-mix(in oklab, var(--tn-accent) calc(var(--tn-near, 0) * 100%), var(--tn-border));\n\tstroke-width: 1.5;\n\tstroke-linecap: round;\n\tstroke-linejoin: round;\n}\n\n.tn-item[data-current] .tn-path {\n\tstroke: var(--tn-accent);\n}\n\n.tn-label {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 0.25rem;\n\twhite-space: nowrap;\n\topacity: 0.4;\n\ttransition:\n\t\tcolor 150ms ease,\n\t\topacity 150ms ease;\n}\n\n.tn-item[data-active] .tn-label,\n.tn-item[data-current] .tn-label {\n\tcolor: var(--tn-accent);\n\topacity: 1;\n}\n\n.tn-item[data-disabled] .tn-link {\n\tcursor: not-allowed;\n}\n\n.tn-item[data-disabled] .tn-label {\n\topacity: 0.2;\n}\n\n.tn-link:focus-visible .tn-label {\n\toutline: 2px solid var(--tn-accent);\n\toutline-offset: 4px;\n\tborder-radius: 2px;\n}\n\n.tn-badge {\n\tfont-size: 0.625rem;\n\topacity: 0.7;\n}\n\n.tn-ticks {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: var(--tn-row-gap);\n}\n\n.tn-tick {\n\tposition: relative;\n\tdisplay: block;\n\theight: 1px;\n\twidth: calc(24px + 16px * var(--tn-tick-near, 0));\n}\n\n.tn-tick svg {\n\tposition: absolute;\n\tleft: 0;\n\ttop: 50%;\n\twidth: 100%;\n\theight: 0.5rem;\n\toverflow: visible;\n\ttransform: translateY(-50%);\n}\n\n.tn-tick-path {\n\tfill: none;\n\tstroke: color-mix(\n\t\tin oklab,\n\t\tvar(--tn-accent) calc(var(--tn-tick-near, 0) * 60%),\n\t\tvar(--tn-border)\n\t);\n\tstroke-width: 1;\n\tstroke-linecap: round;\n}\n\n.tn-preview {\n\tposition: absolute;\n\ttop: 0;\n\tleft: 0;\n\tz-index: 20;\n\twidth: 13rem;\n\taspect-ratio: 16 / 9;\n\toverflow: hidden;\n\tborder: 1px solid color-mix(in oklab, currentColor 10%, transparent);\n\tborder-radius: 0.75rem;\n\tbackground: var(--tn-preview-bg, Canvas);\n\tbox-shadow:\n\t\t0 10px 15px -3px rgb(0 0 0 / 0.1),\n\t\t0 4px 6px -4px rgb(0 0 0 / 0.1);\n\tpointer-events: none;\n\topacity: var(--tn-po, 0);\n\ttransform: translate(var(--tn-px, 0), var(--tn-py, 0));\n}\n/* ------------------------------------------------------------------ neon */\n\n.tn-root[data-variant=\"neon\"] {\n\t--tn-border: color-mix(in oklab, var(--tn-accent) 22%, transparent);\n\t/* The hot core of a lit tube: the accent pushed toward white. */\n\t--tn-hot: color-mix(in oklab, var(--tn-accent) 55%, white);\n}\n\n.tn-root[data-variant=\"neon\"] .tn-bar,\n.tn-root[data-variant=\"neon\"] .tn-tick svg {\n\tfilter: drop-shadow(0 0 calc(1px + var(--tn-near, 0) * 3px) var(--tn-accent))\n\t\tdrop-shadow(0 0 calc(var(--tn-near, 0) * 12px) var(--tn-accent));\n}\n\n.tn-root[data-variant=\"neon\"] .tn-tick svg {\n\tfilter: drop-shadow(0 0 calc(0.5px + var(--tn-tick-near, 0) * 5px) var(--tn-accent));\n}\n\n.tn-root[data-variant=\"neon\"] .tn-path {\n\tstroke: color-mix(in oklab, var(--tn-hot) calc(var(--tn-near, 0) * 100%), var(--tn-border));\n}\n\n.tn-root[data-variant=\"neon\"] .tn-tick-path {\n\tstroke: color-mix(in oklab, var(--tn-hot) calc(var(--tn-tick-near, 0) * 70%), var(--tn-border));\n}\n\n/* The beam: a short, near-white dash centred on the wave's head, with a\n   wide halo. Invisible at rest; the core flags a plucking line. */\n.tn-beam {\n\tfill: none;\n\tstroke: color-mix(in oklab, white 75%, var(--tn-accent));\n\tstroke-width: 2.25;\n\tstroke-linecap: round;\n\tstroke-dasharray: 16 200;\n\tstroke-dashoffset: calc(8 - var(--tn-p, 0) * 100);\n\topacity: 0;\n\tfilter: drop-shadow(0 0 2px var(--tn-accent)) drop-shadow(0 0 8px var(--tn-accent));\n}\n\n/* `data-plucking` is set by the core on the line's <svg>. */\nsvg[data-plucking] .tn-beam {\n\t/* Fades out over the last third of the run as the wave dies down. */\n\topacity: calc(min(1, (1 - var(--tn-p, 0)) * 3));\n}\n\n.tn-tick .tn-beam {\n\tstroke-width: 1.5;\n}\n\n.tn-root[data-variant=\"neon\"] .tn-item[data-current] .tn-path {\n\tstroke: var(--tn-hot);\n}\n\n.tn-root[data-variant=\"neon\"] .tn-item[data-current] .tn-bar {\n\tanimation: tn-breathe 3.2s ease-in-out infinite;\n}\n\n@keyframes tn-breathe {\n\t0%,\n\t100% {\n\t\tfilter: drop-shadow(0 0 2px var(--tn-accent)) drop-shadow(0 0 6px var(--tn-accent));\n\t}\n\t50% {\n\t\tfilter: drop-shadow(0 0 3px var(--tn-accent)) drop-shadow(0 0 12px var(--tn-accent));\n\t}\n}\n\n.tn-root[data-variant=\"neon\"] .tn-item[data-active] .tn-label,\n.tn-root[data-variant=\"neon\"] .tn-item[data-current] .tn-label {\n\tcolor: var(--tn-hot);\n\ttext-shadow:\n\t\t0 0 6px color-mix(in oklab, var(--tn-accent) 70%, transparent),\n\t\t0 0 16px color-mix(in oklab, var(--tn-accent) 45%, transparent);\n}\n\n.tn-root[data-variant=\"neon\"] .tn-preview {\n\tborder-color: color-mix(in oklab, var(--tn-accent) 45%, transparent);\n\tbox-shadow:\n\t\t0 0 0 1px color-mix(in oklab, var(--tn-accent) 20%, transparent),\n\t\t0 0 24px -4px color-mix(in oklab, var(--tn-accent) 60%, transparent);\n}\n\n/* The HDR light layer: brighter-than-white glow, drawn over the lines.\n   Its `color` is only read to resolve the accent for the shader. */\n.tn-hdr {\n\tposition: absolute;\n\ttop: -24px;\n\tleft: -24px;\n\twidth: calc(100% + 48px);\n\theight: calc(100% + 48px);\n\tz-index: 1;\n\tpointer-events: none;\n\tcolor: var(--tn-accent);\n}\n\n@media (prefers-reduced-motion: reduce) {\n\t.tn-root[data-variant=\"neon\"] .tn-item[data-current] .tn-bar {\n\t\tanimation: none;\n\t}\n}\n","type":"registry:ui","target":"@ui/fancy/components/tick-nav/tick-nav.css"},{"path":"react/src/components/tick-nav/types.ts","content":"\"use client\";\n\n/** One row of a TickNav. */\nexport interface TickNavItem {\n\t/** Stable id; matched against `value` to mark the current row. */\n\tid: string;\n\t/** Visible text. */\n\tlabel: string;\n\t/** Renders the row as a link when set; otherwise it is a button. */\n\thref?: string;\n\t/** Short trailing tag, such as \"New\". */\n\tbadge?: string;\n\t/** Greyed out, not activatable, silent. */\n\tdisabled?: boolean;\n}\n","type":"registry:ui","target":"@ui/fancy/components/tick-nav/types.ts"}],"meta":{"license":"MIT","package":"fancy-ui-react","version":"0.7.0","source":"https://fancy-ui.rama.app/docs/components/tick-nav?framework=react"}}