{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "FolderFloat-TS-TW",
	"title": "FolderFloat",
	"description": "Folder whose notes spring out into a floating cloud when it opens on hover or press.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "FolderFloat/FolderFloat.tsx",
			"content": "'use client';\n\nimport React, {\n  useCallback,\n  useEffect,\n  useLayoutEffect,\n  useRef,\n  useState,\n  type CSSProperties,\n  type PointerEvent\n} from 'react';\nimport Matter from 'matter-js';\nconst { Bodies, Body, Composite, Engine } = Matter;\n\nexport type FolderFloatItem = string | { label: string; value: string };\nexport type FolderFloatTrigger = 'hover' | 'click';\n\nexport interface FolderFloatProps {\n  items?: FolderFloatItem[];\n  label?: string;\n  sublabel?: string;\n  trigger?: FolderFloatTrigger;\n  defaultOpen?: boolean;\n  closeOnSelect?: boolean;\n  physics?: boolean;\n  drift?: number;\n  onSelect?: (value: string, index: number) => void;\n  onOpenChange?: (open: boolean) => void;\n  folderColor?: string;\n  frontColor?: string;\n  paperColor?: string;\n  itemColor?: string;\n  itemTextColor?: string;\n  labelColor?: string;\n  width?: number;\n  height?: number;\n  radius?: number;\n  spread?: number;\n  lift?: number;\n  tilt?: number;\n  flapAngle?: number;\n  restAngle?: number;\n  openDuration?: number;\n  stagger?: number;\n  bounce?: number;\n  className?: string;\n}\n\ntype Entry = { label: string; value: string };\ntype Size = { w: number; h: number };\ntype Zone = { left: number; right: number; top: number; bottom: number };\ntype Drag = { i: number; id: number; dx: number; dy: number; sx: number; sy: number; moved: boolean };\ninterface World {\n  engine: Matter.Engine | null;\n  bodies: Matter.Body[];\n  sizes: Size[];\n  raf: number;\n  last: number;\n  t0: number;\n  drag: Drag | null;\n  zone: Zone | null;\n  live: boolean;\n}\ninterface Latest {\n  onSelect?: FolderFloatProps['onSelect'];\n  onOpenChange?: FolderFloatProps['onOpenChange'];\n  drift: number;\n  reduce: boolean;\n}\n\nconst DEFAULT_ITEMS: FolderFloatItem[] = [\n  'Try a warmer palette',\n  'Tighten the spacing',\n  'Logo feels small',\n  'Love the new hero'\n];\nconst PAD = 28;\nconst CHAR = 6.8;\nconst GAP = 12;\nconst ROW = 52;\nconst DRAG_MIN = 4;\nconst ZONE_PAD = 8;\n\nconst jitter = (i: number) => {\n  const x = Math.sin(i * 12.9898 + 4.1414) * 43758.5453;\n  return x - Math.floor(x);\n};\n\nconst layout = (list: Entry[], spread: number, lift: number, tilt: number, sizes: (Size | null)[]) => {\n  const rows: { items: { i: number; pw: number }[]; width: number }[] = [];\n  let row: { i: number; pw: number }[] = [];\n  let width = 0;\n  list.forEach((item, i) => {\n    const pw = sizes[i]?.w ?? PAD + item.label.length * CHAR;\n    if (row.length && width + GAP + pw > spread * 2) {\n      rows.push({ items: row, width });\n      row = [];\n      width = 0;\n    }\n    row.push({ i, pw });\n    width += (row.length > 1 ? GAP : 0) + pw;\n  });\n  if (row.length) rows.push({ items: row, width });\n  const pos: { x: number; y: number; r: number }[] = [];\n  rows.forEach((r, ri) => {\n    let x = -r.width / 2;\n    const shift = (ri % 2 ? 1 : -1) * Math.min(16, spread * 0.1);\n    r.items.forEach(({ i, pw }) => {\n      const j = jitter(i);\n      pos[i] = { x: x + pw / 2 + shift + (j - 0.5) * 6, y: -lift - ri * ROW - j * 6, r: tilt * (j * 2 - 1) };\n      x += pw + GAP;\n    });\n  });\n  return pos;\n};\n\nconst STYLE = `\n@keyframes folder-float-drift { 0%, 100% { translate: 0 0; } 50% { translate: 0 -3px; } }\n@keyframes folder-float-pop { 30% { scale: 1.1; } 100% { scale: 1; } }\n`;\n\nconst FolderFloat: React.FC<FolderFloatProps> = ({\n  items = DEFAULT_ITEMS,\n  label = 'Design feedback',\n  sublabel = '',\n  trigger = 'hover',\n  defaultOpen = false,\n  closeOnSelect = true,\n  physics = true,\n  drift = 0.5,\n  onSelect,\n  onOpenChange,\n  folderColor = '#3f3f46',\n  frontColor = '#52525b',\n  paperColor = '#f5f5f5',\n  itemColor = '#f5f5f5',\n  itemTextColor = '#18181b',\n  labelColor = '#f5f5f5',\n  width = 200,\n  height = 148,\n  radius = 14,\n  spread = 180,\n  lift = 26,\n  tilt = 8,\n  flapAngle = 34,\n  restAngle = 16,\n  openDuration = 520,\n  stagger = 45,\n  bounce = 0.3,\n  className = ''\n}) => {\n  const [open, setOpen] = useState(defaultOpen);\n  const [popped, setPopped] = useState(-1);\n  const [live, setLive] = useState(false);\n  const [sizes, setSizes] = useState<Size[]>([]);\n  const anchorRef = useRef<HTMLDivElement>(null);\n  const pillRefs = useRef<(HTMLButtonElement | null)[]>([]);\n  const world = useRef<World>({\n    engine: null,\n    bodies: [],\n    sizes: [],\n    raf: 0,\n    last: 0,\n    t0: 0,\n    drag: null,\n    zone: null,\n    live: false\n  });\n  const latest = useRef<Latest>({} as Latest);\n  latest.current = { onSelect, onOpenChange, drift, reduce: false };\n  const popTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n  const liveTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);\n  const list: Entry[] = items.map(item => (typeof item === 'string' ? { label: item, value: item } : item));\n  const n = list.length;\n  const sub = sublabel || `${n} ${n === 1 ? 'note' : 'notes'}`;\n  const pos = layout(list, spread, lift, tilt, sizes);\n\n  const labelsKey = list.map(item => item.label).join('|');\n  useLayoutEffect(() => {\n    const measure = () => {\n      const next = pillRefs.current.slice(0, n).map(el => (el ? { w: el.offsetWidth, h: el.offsetHeight } : null));\n      if (next.some(s => !s)) return;\n      const sized = next as Size[];\n      setSizes(prev =>\n        prev.length === sized.length && prev.every((s, i) => s.w === sized[i].w && s.h === sized[i].h) ? prev : sized\n      );\n    };\n    measure();\n    document.fonts?.ready.then(measure);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [n, labelsKey]);\n\n  const stopPhysics = useCallback(() => {\n    const w = world.current;\n    clearTimeout(liveTimer.current);\n    cancelAnimationFrame(w.raf);\n    w.raf = 0;\n    if (w.engine) {\n      w.bodies.forEach((b, i) => {\n        const el = pillRefs.current[i];\n        if (!el) return;\n        el.style.setProperty('--x', `${b.position.x.toFixed(1)}px`);\n        el.style.setProperty('--y', `${(b.position.y - w.sizes[i].h / 2).toFixed(1)}px`);\n      });\n      Composite.clear(w.engine.world, false, true);\n      Engine.clear(w.engine);\n      w.engine = null;\n    }\n    w.bodies = [];\n    w.drag = null;\n    w.live = false;\n    setLive(false);\n  }, []);\n\n  const startPhysics = useCallback(() => {\n    const w = world.current;\n    if (w.engine) return;\n    const els = pillRefs.current.slice(0, n);\n    if (els.some(el => !el)) return;\n    const engine = Engine.create({ gravity: { x: 0, y: 0 } });\n    engine.enableSleeping = false;\n    w.engine = engine;\n    w.sizes = (els as HTMLButtonElement[]).map(el => ({ w: el.offsetWidth, h: el.offsetHeight }));\n    const ys = pos.map(p => p.y);\n    const zone = {\n      left: -spread - ZONE_PAD,\n      right: spread + ZONE_PAD,\n      top: Math.min(...ys) - ZONE_PAD,\n      bottom: -lift + Math.max(...w.sizes.map(s => s.h))\n    };\n    w.zone = zone;\n    w.bodies = els.map((el, i) => {\n      const { w: bw, h: bh } = w.sizes[i];\n      const b = Bodies.rectangle(pos[i].x, pos[i].y + bh / 2, bw, bh, {\n        chamfer: { radius: Math.min(bh / 2 - 1, 16) },\n        restitution: 0.55,\n        friction: 0,\n        frictionAir: 0.08,\n        inertia: Infinity\n      });\n      (b as Matter.Body & { plugin: { phase: number } }).plugin = { phase: jitter(i) * Math.PI * 2 };\n      return b;\n    });\n    const T = 80;\n    const walls = [\n      Bodies.rectangle((zone.left + zone.right) / 2, zone.top - T / 2, zone.right - zone.left + 2 * T, T, {\n        isStatic: true\n      }),\n      Bodies.rectangle((zone.left + zone.right) / 2, zone.bottom + T / 2, zone.right - zone.left + 2 * T, T, {\n        isStatic: true\n      }),\n      Bodies.rectangle(zone.left - T / 2, (zone.top + zone.bottom) / 2, T, zone.bottom - zone.top + 2 * T, {\n        isStatic: true\n      }),\n      Bodies.rectangle(zone.right + T / 2, (zone.top + zone.bottom) / 2, T, zone.bottom - zone.top + 2 * T, {\n        isStatic: true\n      })\n    ];\n    Composite.add(engine.world, [...w.bodies, ...walls]);\n    w.live = true;\n    w.last = 0;\n    w.t0 = performance.now();\n    setLive(true);\n    const tick = (now: number) => {\n      const s = world.current;\n      if (!s.engine) return;\n      const dt = s.last ? Math.min(32, now - s.last) : 16;\n      s.last = now;\n      const t = (now - s.t0) / 1000;\n      const k = latest.current.drift * 0.00005 * Math.min(1, t / 2);\n      s.bodies.forEach((b, i) => {\n        if (s.drag && s.drag.i === i) return;\n        const ph = (b as Matter.Body & { plugin: { phase: number } }).plugin.phase;\n        Body.applyForce(b, b.position, {\n          x: Math.sin(t * 0.9 + ph) * k * b.mass,\n          y: Math.cos(t * 1.3 + ph * 1.7) * k * b.mass\n        });\n      });\n      Engine.update(s.engine, dt);\n      s.bodies.forEach((b, i) => {\n        const el = pillRefs.current[i];\n        if (!el) return;\n        el.style.setProperty('--x', `${b.position.x.toFixed(1)}px`);\n        el.style.setProperty('--y', `${(b.position.y - s.sizes[i].h / 2).toFixed(1)}px`);\n      });\n      s.raf = requestAnimationFrame(tick);\n    };\n    w.raf = requestAnimationFrame(tick);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [n, spread, lift, pos.map(p => `${p.x},${p.y}`).join('|')]);\n\n  const set = useCallback(\n    (next: boolean) => {\n      if (!next) stopPhysics();\n      setOpen(prev => {\n        if (prev === next) return prev;\n        latest.current.onOpenChange?.(next);\n        return next;\n      });\n    },\n    [stopPhysics]\n  );\n\n  useEffect(() => {\n    clearTimeout(liveTimer.current);\n    if (!open || !physics || latest.current.reduce) {\n      if (!open) stopPhysics();\n      else if (!physics) stopPhysics();\n      return undefined;\n    }\n    liveTimer.current = setTimeout(startPhysics, openDuration + (n - 1) * stagger + 80);\n    return () => clearTimeout(liveTimer.current);\n  }, [open, physics, openDuration, stagger, n, startPhysics, stopPhysics]);\n\n  useEffect(() => {\n    const mq = window.matchMedia('(prefers-reduced-motion: reduce)');\n    const sync = () => {\n      latest.current.reduce = mq.matches;\n    };\n    sync();\n    mq.addEventListener('change', sync);\n    return () => mq.removeEventListener('change', sync);\n  }, []);\n\n  useEffect(\n    () => () => {\n      clearTimeout(popTimer.current);\n      stopPhysics();\n    },\n    [stopPhysics]\n  );\n\n  const pick = (item: Entry, i: number) => {\n    latest.current.onSelect?.(item.value, i);\n    clearTimeout(popTimer.current);\n    setPopped(i);\n    popTimer.current = setTimeout(() => setPopped(-1), 320);\n    if (closeOnSelect) set(false);\n  };\n\n  const pointerAt = (e: PointerEvent<HTMLButtonElement>) => {\n    const r = anchorRef.current?.getBoundingClientRect();\n    return r ? { x: e.clientX - r.left, y: e.clientY - r.top } : { x: 0, y: 0 };\n  };\n  const down = (e: PointerEvent<HTMLButtonElement>, i: number) => {\n    const w = world.current;\n    if (!w.live || e.button !== 0) return;\n    const b = w.bodies[i];\n    if (!b) return;\n    const p = pointerAt(e);\n    w.drag = {\n      i,\n      id: e.pointerId,\n      dx: b.position.x - p.x,\n      dy: b.position.y - p.y,\n      sx: e.clientX,\n      sy: e.clientY,\n      moved: false\n    };\n    try {\n      e.currentTarget.setPointerCapture(e.pointerId);\n    } catch {}\n  };\n  const move = (e: PointerEvent<HTMLButtonElement>, i: number) => {\n    const w = world.current;\n    const d = w.drag;\n    if (!d || d.i !== i || d.id !== e.pointerId) return;\n    if (!d.moved && Math.hypot(e.clientX - d.sx, e.clientY - d.sy) >= DRAG_MIN) {\n      d.moved = true;\n      e.currentTarget.setAttribute('data-drag', '');\n    }\n    if (!d.moved) return;\n    const b = w.bodies[i];\n    const { w: bw, h: bh } = w.sizes[i];\n    const z = w.zone as Zone;\n    const p = pointerAt(e);\n    const x = Math.min(z.right - bw / 2, Math.max(z.left + bw / 2, p.x + d.dx));\n    const y = Math.min(z.bottom - bh / 2, Math.max(z.top + bh / 2, p.y + d.dy));\n    Body.setVelocity(b, { x: (x - b.position.x) * 0.6, y: (y - b.position.y) * 0.6 });\n    Body.setPosition(b, { x, y });\n  };\n  const up = (e: PointerEvent<HTMLButtonElement>, i: number, item: Entry) => {\n    const w = world.current;\n    const d = w.drag;\n    if (!d || d.i !== i || d.id !== e.pointerId) return;\n    w.drag = null;\n    e.currentTarget.removeAttribute('data-drag');\n    try {\n      e.currentTarget.releasePointerCapture(e.pointerId);\n    } catch {}\n    if (!d.moved && e.type === 'pointerup') pick(item, i);\n  };\n\n  const hover = trigger === 'hover';\n\n  return (\n    <div\n      className={`group relative inline-block text-[13px] leading-none font-medium [width:var(--ff-w)] [padding-top:var(--ff-tab)] [font-family:inherit]${className ? ` ${className}` : ''}`}\n      data-open={open ? '' : undefined}\n      data-live={live ? '' : undefined}\n      data-physics={physics ? '' : undefined}\n      data-trigger={trigger}\n      onPointerEnter={hover ? () => set(true) : undefined}\n      onPointerLeave={\n        hover\n          ? () => {\n              if (!world.current.drag) set(false);\n            }\n          : undefined\n      }\n      onKeyDown={(e: React.KeyboardEvent<HTMLDivElement>) => {\n        if (e.key === 'Escape' && open) {\n          e.stopPropagation();\n          set(false);\n        }\n      }}\n      style={\n        {\n          '--ff-w': `${width}px`,\n          '--ff-h': `${height}px`,\n          '--ff-r': `${radius}px`,\n          '--ff-back': folderColor,\n          '--ff-front': frontColor,\n          '--ff-paper': paperColor,\n          '--ff-item': itemColor,\n          '--ff-item-ink': itemTextColor,\n          '--ff-label': labelColor,\n          '--ff-spread': `${spread}px`,\n          '--ff-lift': `${lift}px`,\n          '--ff-angle': `${flapAngle}deg`,\n          '--ff-rest': `${restAngle}deg`,\n          '--ff-open': `${openDuration}ms`,\n          '--ff-close': `${Math.round(openDuration * 0.6)}ms`,\n          '--ff-stagger': `${stagger}ms`,\n          '--ff-n': n,\n          '--ff-tab': '14px',\n          '--ff-ease-out': 'cubic-bezier(0.23, 1, 0.32, 1)',\n          '--ff-spring': `cubic-bezier(0.34, ${(1 + bounce * 1.9).toFixed(2)}, 0.64, 1)`\n        } as CSSProperties\n      }\n    >\n      <style>{STYLE}</style>\n      <div\n        ref={anchorRef}\n        className=\"absolute top-[var(--ff-tab)] left-1/2 z-[1] h-0 w-0 group-data-[open]:before:absolute group-data-[open]:before:top-[calc(-1*(var(--ff-lift)+120px))] group-data-[open]:before:left-[calc(-1*(var(--ff-spread)+100px))] group-data-[open]:before:h-[calc(var(--ff-lift)+120px)] group-data-[open]:before:w-[calc(2*var(--ff-spread)+200px)] group-data-[open]:before:content-['']\"\n      >\n        {list.map((item, i) => {\n          const p = pos[i];\n          return (\n            <button\n              key={`${item.value}-${i}`}\n              ref={el => {\n                pillRefs.current[i] = el;\n              }}\n              type=\"button\"\n              className=\"pointer-events-none absolute top-0 left-1/2 m-0 h-[34px] cursor-pointer rounded-[17px] border-0 px-3.5 whitespace-nowrap opacity-0 shadow-[0_4px_12px_rgba(0,0,0,0.14)] outline-none [background:var(--ff-item)] [color:var(--ff-item-ink)] [font:inherit] [transform:translate(-50%,44px)_scale(0.6)] [transform-origin:50%_50%] [-webkit-tap-highlight-color:transparent] [transition:transform_var(--ff-close)_var(--ff-ease-out)_calc((var(--ff-n)-1-var(--i))*var(--ff-stagger)*0.5),opacity_160ms_ease_calc((var(--ff-n)-1-var(--i))*var(--ff-stagger)*0.5+var(--ff-close)*0.45),scale_160ms_var(--ff-ease-out)] group-data-[open]:pointer-events-auto group-data-[open]:opacity-100 group-data-[open]:[transform:translate(calc(-50%+var(--x)),var(--y))_rotate(var(--r))_scale(1)] group-data-[open]:[transition:transform_var(--ff-open)_var(--ff-spring)_calc(var(--i)*var(--ff-stagger)),opacity_160ms_ease_calc(var(--i)*var(--ff-stagger)),scale_160ms_var(--ff-ease-out)] group-data-[live]:cursor-grab group-data-[live]:[transition:scale_160ms_var(--ff-ease-out)] data-[drag]:cursor-grabbing! group-data-[open]:hover:[scale:1.05] group-data-[open]:active:[scale:0.97] data-[pop]:[animation:folder-float-pop_320ms_var(--ff-ease-out)] motion-reduce:[transition:opacity_200ms_ease] motion-reduce:group-data-[open]:[transition:opacity_200ms_ease_calc(var(--i)*var(--ff-stagger))]\"\n              tabIndex={open ? 0 : -1}\n              aria-hidden={!open}\n              data-pop={popped === i ? '' : undefined}\n              style={\n                {\n                  '--i': i,\n                  '--x': `${p.x.toFixed(1)}px`,\n                  '--y': `${p.y.toFixed(1)}px`,\n                  '--r': `${p.r.toFixed(2)}deg`\n                } as CSSProperties\n              }\n              onPointerDown={e => down(e, i)}\n              onPointerMove={e => move(e, i)}\n              onPointerUp={e => up(e, i, item)}\n              onPointerCancel={e => up(e, i, item)}\n              onClick={e => {\n                if (!world.current.live || e.detail === 0) pick(item, i);\n              }}\n            >\n              <span className=\"block [animation:folder-float-drift_3.2s_ease-in-out_infinite] [animation-delay:calc(var(--i)*-0.7s)] [animation-play-state:paused] group-data-[open]:[animation-play-state:running] group-data-[physics]:[animation:none] group-data-[live]:[animation:none] motion-reduce:[animation:none]\">\n                {item.label}\n              </span>\n            </button>\n          );\n        })}\n      </div>\n      <div className=\"relative [width:var(--ff-w)] [height:var(--ff-h)]\">\n        <span\n          className=\"absolute inset-0 z-0 [border-radius:var(--ff-r)] [background:var(--ff-back)] [transform:perspective(600px)_rotateX(8deg)] [transform-origin:50%_100%] before:absolute before:top-[calc(-1*var(--ff-tab))] before:left-0 before:h-[calc(var(--ff-tab)+var(--ff-r))] before:w-[42%] before:[border-radius:var(--ff-r)_var(--ff-r)_0_0] before:[background:inherit] before:content-['']\"\n          aria-hidden=\"true\"\n        />\n        <span\n          className=\"absolute top-[10%] right-[8%] left-[8%] z-[1] h-1/2 rounded-md opacity-0 [background:var(--ff-paper)] [transform:translateY(10px)] [transition:transform_var(--ff-close)_var(--ff-ease-out),opacity_var(--ff-close)_ease] group-data-[open]:opacity-100 group-data-[open]:[transform:translateY(0)] group-data-[open]:[transition:transform_var(--ff-open)_var(--ff-ease-out),opacity_200ms_ease] motion-reduce:[transform:none]! motion-reduce:[transition:opacity_200ms_ease]\"\n          aria-hidden=\"true\"\n        />\n        <span\n          className=\"absolute right-0 bottom-0 left-0 z-[2] box-border flex h-[76%] flex-col justify-end gap-[5px] px-4 py-3.5 [border-radius:var(--ff-r)] [background:linear-gradient(180deg,color-mix(in_srgb,var(--ff-front)_92%,#fff),var(--ff-front)_60%)] [color:var(--ff-label)] shadow-[0_-10px_24px_rgba(0,0,0,0.28)] [transform:perspective(600px)_rotateX(calc(-1*var(--ff-rest)))] [transform-origin:50%_100%] [transition:transform_var(--ff-open)_var(--ff-ease-out)] group-data-[open]:[transform:perspective(600px)_rotateX(calc(-1*var(--ff-angle)))] motion-reduce:group-data-[open]:[transform:perspective(600px)_rotateX(calc(-1*var(--ff-rest)))] motion-reduce:[transition:opacity_200ms_ease]\"\n          aria-hidden=\"true\"\n        >\n          <span className=\"text-[13px] font-medium\">{label}</span>\n          <span className=\"text-[11px] opacity-55\">{sub}</span>\n        </span>\n        <button\n          type=\"button\"\n          className=\"absolute right-0 bottom-0 left-0 z-[3] m-0 h-[76%] cursor-pointer border-0 bg-transparent p-0 outline-none [border-radius:var(--ff-r)] [-webkit-tap-highlight-color:transparent]\"\n          aria-expanded={open}\n          aria-label={`${label}, ${sub}`}\n          onClick={() => set(!open)}\n        />\n      </div>\n    </div>\n  );\n};\n\nexport default FolderFloat;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"matter-js@^0.20.0"
	],
	"devDependencies": [
		"@types/matter-js@^0.19.8"
	]
}