{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "GlideSelect-JS-CSS",
	"title": "GlideSelect",
	"description": "Select chip whose menu pops from its corner and whose highlight glides between rows.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:file",
			"path": "GlideSelect.css",
			"target": "@components/GlideSelect.css",
			"content": ".glide-select {\n  --gs-accent: #f5f5f5;\n  --gs-surface: #27272a;\n  --gs-highlight: #3f3f46;\n  --gs-text: #f5f5f5;\n  --gs-radius: 10px;\n  --gs-inner-radius: 6px;\n  --gs-chip: 32px;\n  --gs-row: 30px;\n  --gs-font: 13px;\n  --gs-menu-w: 176px;\n  --gs-pop: 180ms;\n  --gs-pop-out: 120ms;\n  --gs-glide: 220ms;\n  --gs-origin: top left;\n  --gs-ease-out: cubic-bezier(0.23, 1, 0.32, 1);\n\n  position: relative;\n  display: inline-block;\n}\n\n.glide-select[data-disabled] {\n  opacity: 0.5;\n}\n\n.glide-select__trigger {\n  position: relative;\n  display: inline-flex;\n  align-items: center;\n  gap: 6px;\n  height: var(--gs-chip);\n  margin: 0;\n  padding: 0 8px 0 10px;\n  border: 0;\n  border-radius: var(--gs-inner-radius);\n  background: var(--gs-surface);\n  color: var(--gs-text);\n  font-family: inherit;\n  font-size: var(--gs-font);\n  font-weight: 500;\n  line-height: 1;\n  cursor: pointer;\n  outline: none;\n  user-select: none;\n  -webkit-user-select: none;\n  -webkit-tap-highlight-color: transparent;\n  touch-action: manipulation;\n  transition:\n    background-color 100ms ease,\n    transform 160ms var(--gs-ease-out);\n}\n\n.glide-select__trigger:disabled {\n  cursor: default;\n}\n\n@media (hover: hover) and (pointer: fine) {\n  .glide-select__trigger:not(:disabled):hover {\n    background: color-mix(in srgb, var(--gs-highlight) 60%, var(--gs-surface));\n  }\n}\n\n.glide-select__trigger[aria-expanded='true'] {\n  background: color-mix(in srgb, var(--gs-highlight) 60%, var(--gs-surface));\n}\n\n.glide-select__trigger:not(:disabled):active {\n  transform: scale(0.97);\n}\n\n.glide-select__label[data-empty] {\n  opacity: 0.6;\n}\n\n.glide-select[data-swap] .glide-select__label {\n  animation: gs-swap 160ms ease;\n}\n\n@keyframes gs-swap {\n  from {\n    opacity: 0.6;\n    filter: blur(2px);\n  }\n}\n\n.glide-select__chevron {\n  display: inline-flex;\n  color: color-mix(in srgb, var(--gs-text) 55%, transparent);\n  transition: transform 200ms var(--gs-ease-out);\n}\n\n.glide-select__trigger[aria-expanded='true'] .glide-select__chevron {\n  transform: rotate(180deg);\n}\n\n.glide-select__menu {\n  position: absolute;\n  z-index: 20;\n  width: var(--gs-menu-w);\n  min-width: 100%;\n  padding: 4px;\n  border-radius: var(--gs-radius);\n  background: var(--gs-surface);\n  box-shadow: 0 4px 16px color-mix(in srgb, #000 22%, transparent);\n  transform-origin: var(--gs-origin);\n  opacity: 0;\n  transform: scale(0.95);\n  transition:\n    opacity var(--gs-pop) var(--gs-ease-out),\n    transform var(--gs-pop) var(--gs-ease-out);\n}\n\n.glide-select__menu[data-state='open'] {\n  opacity: 1;\n  transform: none;\n}\n\n.glide-select__menu[data-state='closed'] {\n  transition-duration: var(--gs-pop-out);\n  pointer-events: none;\n}\n\n.glide-select__menu[data-side='bottom'] {\n  top: calc(100% + 6px);\n}\n\n.glide-select__menu[data-side='top'] {\n  bottom: calc(100% + 6px);\n}\n\n.glide-select__menu[data-align='left'] {\n  left: 0;\n}\n\n.glide-select__menu[data-align='right'] {\n  right: 0;\n}\n\n.glide-select__list {\n  position: relative;\n  display: grid;\n  gap: 1px;\n  touch-action: none;\n}\n\n.glide-select__pill {\n  position: absolute;\n  top: 0;\n  right: 0;\n  left: 0;\n  height: var(--gs-row);\n  border-radius: var(--gs-inner-radius);\n  background: var(--gs-highlight);\n  opacity: 0;\n  pointer-events: none;\n  transition:\n    transform var(--gs-glide) var(--gs-ease-out),\n    opacity 150ms ease;\n}\n\n.glide-select__option {\n  position: relative;\n  z-index: 1;\n  display: flex;\n  align-items: center;\n  gap: 8px;\n  height: var(--gs-row);\n  padding: 0 8px 0 10px;\n  border-radius: var(--gs-inner-radius);\n  color: var(--gs-text);\n  font-size: var(--gs-font);\n  cursor: pointer;\n  user-select: none;\n  -webkit-user-select: none;\n  -webkit-tap-highlight-color: transparent;\n  transition: background-color 150ms ease;\n}\n\n.glide-select__option[aria-selected='true'] {\n  background: color-mix(in srgb, var(--gs-highlight) 60%, transparent);\n}\n\n.glide-select__list[data-live] .glide-select__option[aria-selected='true'] {\n  background: transparent;\n}\n\n.glide-select__name {\n  flex: 1 1 auto;\n  min-width: 0;\n  overflow: hidden;\n  font-weight: 500;\n  white-space: nowrap;\n  text-overflow: ellipsis;\n}\n\n.glide-select__tag {\n  flex-shrink: 0;\n  color: color-mix(in srgb, var(--gs-text) 55%, transparent);\n  font-size: calc(var(--gs-font) - 2px);\n}\n\n.glide-select__check {\n  display: inline-flex;\n  flex-shrink: 0;\n  color: var(--gs-accent);\n  visibility: hidden;\n}\n\n.glide-select__check[data-on] {\n  visibility: visible;\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .glide-select__menu {\n    transform: none !important;\n    transition: opacity var(--gs-pop) ease;\n  }\n\n  .glide-select__menu[data-state='closed'] {\n    transition-duration: var(--gs-pop-out);\n  }\n\n  .glide-select__pill {\n    transition: opacity 150ms ease;\n  }\n\n  .glide-select__chevron {\n    transition: none;\n  }\n\n  .glide-select__trigger {\n    transition: background-color 100ms ease;\n  }\n\n  .glide-select__trigger:not(:disabled):active {\n    transform: none;\n  }\n\n  .glide-select[data-swap] .glide-select__label {\n    animation: none;\n  }\n}\n"
		},
		{
			"type": "registry:component",
			"path": "GlideSelect.jsx",
			"content": "'use client';\n\nimport { useEffect, useId, useLayoutEffect, useRef, useState } from 'react';\nimport { HugeiconsIcon } from '@hugeicons/react';\nimport { ArrowDown01Icon, Tick02Icon } from '@hugeicons/core-free-icons';\n\nimport './GlideSelect.css';\n\nconst SIZES = {\n  sm: { chip: 28, row: 26, font: 12 },\n  md: { chip: 32, row: 30, font: 13 },\n  lg: { chip: 44, row: 40, font: 14 }\n};\nconst PAD = 4;\nconst GAP = 1;\nconst MENU_GAP = 6;\nconst DEFAULT_OPTIONS = ['One', 'Two', 'Three'];\n\nconst norm = o => (typeof o === 'string' ? { value: o, label: o } : o);\nconst textOf = it => (typeof it.label === 'string' ? it.label : it.value);\nconst typeaheadIndex = (items, from, ch) => {\n  const c = ch.toLowerCase();\n  const n = items.length;\n  for (let k = 1; k <= n; k++) {\n    const i = (from + k) % n;\n    if (textOf(items[i]).toLowerCase().startsWith(c)) return i;\n  }\n  return from;\n};\n\nexport default function GlideSelect({\n  options = DEFAULT_OPTIONS,\n  value,\n  defaultValue,\n  onChange,\n  placeholder = 'Select…',\n  showTags = true,\n  accentColor = '#f5f5f5',\n  surfaceColor = '#27272a',\n  highlightColor = '#3f3f46',\n  textColor = '#f5f5f5',\n  size = 'md',\n  radius = 10,\n  menuWidth = 176,\n  placement = 'bottom',\n  align = 'left',\n  popDuration = 180,\n  glideDuration = 220,\n  rememberPosition = true,\n  disabled = false,\n  ariaLabel = 'Select',\n  className = ''\n}) {\n  const items = options.map(norm);\n  const [inner, setInner] = useState(defaultValue ?? '');\n  const current = value ?? inner;\n  const selected = items.findIndex(it => it.value === current);\n  const [phase, setPhase] = useState('closed');\n  const [active, setActive] = useState(null);\n  const [side, setSide] = useState(placement);\n  const rootRef = useRef(null);\n  const triggerRef = useRef(null);\n  const menuRef = useRef(null);\n  const pillRef = useRef(null);\n  const instant = useRef(false);\n  const closeTimer = useRef(undefined);\n  const scrub = useRef(null);\n  const id = useId();\n  const S = SIZES[size] ?? SIZES.md;\n  const step = S.row + GAP;\n  const popOut = Math.round((popDuration * 2) / 3);\n\n  useLayoutEffect(() => {\n    if (phase !== 'open') return;\n    const el = menuRef.current;\n    const root = rootRef.current;\n    if (!el || !root) return;\n    const r = root.getBoundingClientRect();\n    const need = el.offsetHeight + MENU_GAP;\n    setSide(\n      placement === 'bottom' && r.bottom + need > window.innerHeight\n        ? 'top'\n        : placement === 'top' && r.top - need < 0\n          ? 'bottom'\n          : placement\n    );\n    el.style.transitionDuration = instant.current ? '0ms' : '';\n    el.dataset.state = 'closed';\n    void el.offsetHeight;\n    el.dataset.state = 'open';\n    const p = pillRef.current;\n    if (p) {\n      p.style.transition = 'none';\n      p.style.transform = `translateY(${Math.max(0, selected) * step}px)`;\n      p.style.opacity = '0';\n      void p.offsetHeight;\n      p.style.transition = '';\n    }\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [phase]);\n\n  useLayoutEffect(() => {\n    const p = pillRef.current;\n    if (!p || phase !== 'open') return;\n    if (active === null) {\n      p.style.opacity = '0';\n      return;\n    }\n    const jump = instant.current || p.style.opacity !== '1';\n    p.style.transitionDuration = jump ? '0ms, 150ms' : '';\n    p.style.transform = `translateY(${active * step}px)`;\n    p.style.opacity = '1';\n    instant.current = false;\n  }, [active, phase, step]);\n\n  const open = viaKey => {\n    if (disabled) return;\n    clearTimeout(closeTimer.current);\n    instant.current = true;\n    setActive(selected >= 0 ? selected : viaKey ? 0 : null);\n    setPhase('open');\n  };\n  const close = mode => {\n    setActive(null);\n    clearTimeout(closeTimer.current);\n    const el = menuRef.current;\n    if (mode === 'instant' || !el) {\n      setPhase('closed');\n      return;\n    }\n    el.style.transitionDuration = '';\n    el.dataset.state = 'closed';\n    setPhase('closing');\n    closeTimer.current = setTimeout(() => setPhase('closed'), popOut + 20);\n  };\n  const pick = (i, viaKey) => {\n    const it = items[i];\n    if (!it) {\n      close('instant');\n      return;\n    }\n    if (it.value !== current) {\n      if (value === undefined) setInner(it.value);\n      onChange?.(it.value, it);\n      if (!viaKey && rootRef.current) rootRef.current.dataset.swap = '';\n    }\n    close('instant');\n    triggerRef.current?.focus({ preventScroll: true });\n  };\n\n  const onTriggerKey = e => {\n    const k = e.key;\n    const n = items.length;\n    const cur = active ?? Math.max(0, selected);\n    if (phase !== 'open') {\n      if (k === 'Enter' || k === ' ' || k === 'ArrowDown' || k === 'ArrowUp') {\n        e.preventDefault();\n        open(true);\n      }\n      return;\n    }\n    const go = i => {\n      e.preventDefault();\n      instant.current = true;\n      setActive(Math.min(n - 1, Math.max(0, i)));\n    };\n    if (k === 'ArrowDown' || k === 'ArrowUp') go(active === null ? cur : cur + (k === 'ArrowDown' ? 1 : -1));\n    else if (k === 'Home' || k === 'End') go(k === 'Home' ? 0 : n - 1);\n    else if (k === 'Enter' || k === ' ') {\n      e.preventDefault();\n      pick(cur, true);\n    } else if (k === 'Escape' || k === 'Tab') {\n      if (k === 'Escape') e.preventDefault();\n      close('instant');\n    } else if (k.length === 1 && !e.metaKey && !e.ctrlKey && !e.altKey) go(typeaheadIndex(items, cur, k));\n  };\n\n  useEffect(() => {\n    if (phase === 'closed') return undefined;\n    const onDown = e => {\n      if (rootRef.current && !rootRef.current.contains(e.target)) close('pop');\n    };\n    document.addEventListener('pointerdown', onDown, true);\n    return () => document.removeEventListener('pointerdown', onDown, true);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [phase]);\n  useEffect(() => {\n    if (disabled && phase !== 'closed') close('instant');\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [disabled]);\n  useEffect(() => () => clearTimeout(closeTimer.current), []);\n\n  const rowAt = y => {\n    const s = scrub.current;\n    if (!s) return null;\n    const i = Math.floor((y - s.top - PAD) / step);\n    return i >= 0 && i < items.length ? i : null;\n  };\n  const onListDown = e => {\n    if (scrub.current) return;\n    try {\n      e.currentTarget.setPointerCapture(e.pointerId);\n    } catch {}\n    scrub.current = { id: e.pointerId, top: e.currentTarget.getBoundingClientRect().top };\n    instant.current = true;\n    setActive(rowAt(e.clientY));\n  };\n  const onListMove = e => {\n    if (!scrub.current || scrub.current.id !== e.pointerId) return;\n    const i = rowAt(e.clientY);\n    if (i !== active) setActive(i);\n  };\n  const onListUp = e => {\n    if (!scrub.current || scrub.current.id !== e.pointerId) return;\n    const i = e.type === 'pointerup' ? rowAt(e.clientY) : null;\n    scrub.current = null;\n    if (i !== null) pick(i, false);\n    else if (!rememberPosition) setActive(null);\n  };\n  const onListOver = e => {\n    if (e.pointerType === 'touch' || scrub.current) return;\n    const row = e.target.closest('[data-index]');\n    if (!row) return;\n    const i = Number(row.dataset.index);\n    if (i !== active) setActive(i);\n  };\n\n  const origin = `${side === 'bottom' ? 'top' : 'bottom'} ${align}`;\n  return (\n    <div\n      ref={rootRef}\n      className={`glide-select${className ? ` ${className}` : ''}`}\n      data-size={size}\n      data-disabled={disabled ? '' : undefined}\n      style={{\n        '--gs-accent': accentColor,\n        '--gs-surface': surfaceColor,\n        '--gs-highlight': highlightColor,\n        '--gs-text': textColor,\n        '--gs-radius': `${radius}px`,\n        '--gs-inner-radius': `${Math.max(3, radius - 4)}px`,\n        '--gs-chip': `${S.chip}px`,\n        '--gs-row': `${S.row}px`,\n        '--gs-font': `${S.font}px`,\n        '--gs-menu-w': `${menuWidth}px`,\n        '--gs-pop': `${popDuration}ms`,\n        '--gs-pop-out': `${popOut}ms`,\n        '--gs-glide': `${glideDuration}ms`,\n        '--gs-origin': origin\n      }}\n      onAnimationEnd={e => {\n        if (e.animationName === 'gs-swap' && rootRef.current) delete rootRef.current.dataset.swap;\n      }}\n    >\n      <button\n        ref={triggerRef}\n        type=\"button\"\n        role=\"combobox\"\n        aria-haspopup=\"listbox\"\n        aria-expanded={phase === 'open'}\n        aria-controls={`${id}-list`}\n        aria-activedescendant={active !== null ? `${id}-${active}` : undefined}\n        aria-label={ariaLabel}\n        disabled={disabled}\n        className=\"glide-select__trigger\"\n        onPointerDown={e => {\n          if (e.button !== 0 || disabled) return;\n          e.currentTarget.focus({ preventScroll: true });\n          if (phase === 'open') close('pop');\n          else open(false);\n        }}\n        onKeyDown={onTriggerKey}\n      >\n        <span className=\"glide-select__label\" key={current} data-empty={selected < 0 ? '' : undefined}>\n          {selected >= 0 ? items[selected].label : placeholder}\n        </span>\n        <span className=\"glide-select__chevron\" aria-hidden=\"true\">\n          <HugeiconsIcon icon={ArrowDown01Icon} size={12} strokeWidth={2.5} />\n        </span>\n      </button>\n      {phase !== 'closed' ? (\n        <div ref={menuRef} className=\"glide-select__menu\" data-state=\"open\" data-side={side} data-align={align}>\n          <div\n            id={`${id}-list`}\n            role=\"listbox\"\n            aria-label={ariaLabel}\n            className=\"glide-select__list\"\n            data-live={active !== null ? '' : undefined}\n            onPointerOver={onListOver}\n            onPointerLeave={() => {\n              if (!scrub.current && !rememberPosition) setActive(null);\n            }}\n            onPointerDown={onListDown}\n            onPointerMove={onListMove}\n            onPointerUp={onListUp}\n            onPointerCancel={onListUp}\n            onLostPointerCapture={onListUp}\n          >\n            <span ref={pillRef} className=\"glide-select__pill\" aria-hidden=\"true\" />\n            {items.map((it, i) => (\n              <div\n                key={it.value}\n                id={`${id}-${i}`}\n                role=\"option\"\n                aria-selected={i === selected}\n                data-index={i}\n                className=\"glide-select__option\"\n              >\n                <span className=\"glide-select__name\">{it.label}</span>\n                {showTags && it.tag ? <span className=\"glide-select__tag\">{it.tag}</span> : null}\n                <span className=\"glide-select__check\" data-on={i === selected ? '' : undefined} aria-hidden=\"true\">\n                  <HugeiconsIcon icon={Tick02Icon} size={13} strokeWidth={2.5} />\n                </span>\n              </div>\n            ))}\n          </div>\n        </div>\n      ) : null}\n    </div>\n  );\n}\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"@hugeicons/react@^1.1.10",
		"@hugeicons/core-free-icons@^4.3.3"
	]
}