{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dot-sort",
  "title": "Dot Sort",
  "description": "Dots sort into rows by group, the logo row largest at 41%.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/dot-sort/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont as loadSans } from \"@remotion/google-fonts/InstrumentSans\";\nimport { loadFont as loadSerif } from \"@remotion/google-fonts/InstrumentSerif\";\nimport { loadFont as loadManrope } from \"@remotion/google-fonts/Manrope\";\nimport type React from \"react\";\nimport { useId, useMemo } from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  interpolate,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst { fontFamily: sans } = loadSans(\"normal\", {\n  weights: [\"400\", \"500\"],\n  subsets: [\"latin\"],\n});\nconst { fontFamily: serif } = loadSerif(\"normal\", {\n  weights: [\"400\"],\n  subsets: [\"latin\"],\n});\nconst manrope = loadManrope(\"normal\", {\n  weights: [\"500\", \"600\"],\n  subsets: [\"latin\"],\n});\n\nconst REMOCN_R_PATH =\n  \"M 0.01 0.81 C 0.01 1.73, 0.36 2.79, 1.09 4.13 C 4.91 11.04, 13.45 17.16, 21.7 18.9 C 22.94 19.16, 23.18 19.16, 51.39 19.27 C 76.07 19.36, 80.02 19.4, 81.32 19.57 C 89.89 20.69, 96.2 24.68, 99.38 31.01 C 103.19 38.56, 102.53 50.1, 97.91 57.07 C 94.66 61.96, 89.68 64.99, 83.26 66 C 82.81 66.07, 70.18 66.83, 55.2 67.69 C 24.82 69.43, 27.03 69.23, 24.18 70.4 C 19.9 72.15, 14.84 75.7, 10.65 79.89 C 4.86 85.68, 1.3 91.91, 0.25 98.13 C 0.12 98.85, 0.08 103.13, 0.04 116.66 L 0 134.26 9.5 134.26 L 19 134.26 19.05 119.41 C 19.1 103.27, 19.08 103.82, 19.82 101.04 C 21.79 93.65, 27.86 88.75, 36.45 87.63 C 37.23 87.53, 39.41 87.5, 43.57 87.53 C 50.12 87.59, 50.75 87.65, 53.22 88.45 C 56.61 89.56, 59.67 91.86, 62.02 95.07 C 62.52 95.76, 69.35 103.85, 77.2 113.07 C 85.04 122.28, 91.63 130.04, 91.85 130.32 C 92.07 130.59, 92.5 131.34, 92.82 131.97 C 93.52 133.35, 94.11 133.93, 95.06 134.13 C 95.5 134.23, 98.97 134.26, 106.36 134.23 L 117.01 134.19 100.82 113.07 C 91.91 101.45, 84.52 91.78, 84.39 91.57 C 83.36 89.89, 83.66 87.53, 85.09 86 C 85.79 85.25, 86.36 84.94, 88.07 84.38 C 96.18 81.72, 104.15 76.62, 109.97 70.36 C 120.59 58.93, 124.06 44.32, 119.44 30.43 C 114.59 15.81, 101.93 4.02, 87.64 0.83 C 83.93 0.01, 88.09 0.08, 41.85 0.04 L 0.01 0 0.01 0.81 Z\";\n\n/** Full logo as on remocn.dev: R mark (0.76em, on the baseline) + \"emocn\" in Manrope 600. */\nfunction RemocnLogo({ fontSize, color }: { fontSize: number; color: string }) {\n  return (\n    <div\n      style={{\n        display: \"flex\",\n        alignItems: \"baseline\",\n        color,\n        fontFamily: manrope.fontFamily,\n        fontWeight: 600,\n        fontSize,\n        letterSpacing: \"-0.025em\",\n        lineHeight: 1,\n      }}\n    >\n      <svg\n        viewBox=\"0 0 124.06 134.26\"\n        style={{ height: \"0.76em\", width: \"auto\" }}\n        aria-hidden\n      >\n        <path fill=\"currentColor\" d={REMOCN_R_PATH} />\n      </svg>\n      <span style={{ marginLeft: \"0.04em\" }}>emocn</span>\n    </div>\n  );\n}\n\nconst isRemocn = (label: string) => label.trim().toLowerCase() === \"remocn\";\n\n/** gradients.sh \"aurora\" palette, laid along the hero series. */\nconst AURORA = {\n  dark: [\"#A0F0C8\", \"#3FB5A3\", \"#5147B6\", \"#B25C9C\"],\n  light: [\"#2E9C86\", \"#3FB5A3\", \"#5147B6\", \"#B25C9C\"],\n} as const;\n\nfunction mix(a: string, b: string, k: number) {\n  const pa = Number.parseInt(a.slice(1), 16);\n  const pb = Number.parseInt(b.slice(1), 16);\n  const ch = (v: number, sh: number) => (v >> sh) & 255;\n  const c = (sh: number) =>\n    Math.round(ch(pa, sh) + (ch(pb, sh) - ch(pa, sh)) * k)\n      .toString(16)\n      .padStart(2, \"0\");\n  return `#${c(16)}${c(8)}${c(0)}`;\n}\n\nfunction sample(stops: readonly string[], k: number) {\n  const x = Math.min(1, Math.max(0, k)) * (stops.length - 1);\n  const i = Math.min(stops.length - 2, Math.floor(x));\n  return mix(stops[i], stops[i + 1], x - i);\n}\n\nexport interface DotSortGroup {\n  label: string;\n  value: number;\n}\n\nexport interface DotSortProps {\n  groups?: DotSortGroup[];\n  highlight?: number;\n  caption?: string;\n  accent?: string;\n  theme?: \"dark\" | \"light\";\n  speed?: number;\n}\n\nconst DEFAULT_GROUPS: DotSortGroup[] = [\n  { label: \"remocn\", value: 41 },\n  { label: \"Kitebase\", value: 23 },\n  { label: \"Lumen\", value: 17 },\n  { label: \"Fernway\", value: 12 },\n  { label: \"Other\", value: 7 },\n];\n\nconst EASE_IO = Easing.bezier(0.65, 0, 0.35, 1);\nconst EASE_OUT = Easing.bezier(0.22, 1, 0.36, 1);\nconst FLIGHT = Easing.bezier(0.5, 0, 0.2, 1);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\nconst THEMES = {\n  dark: {\n    bg: \"#0A0A0B\",\n    fg: \"#F3F2EF\",\n    muted: \"#8B8A90\",\n    grays: [\"#C9C7C2\", \"#8E8D93\", \"#69686F\", \"#4E4D53\"],\n    grain: 0.06,\n  },\n  light: {\n    bg: \"#F5F4F0\",\n    fg: \"#121214\",\n    muted: \"#6E6C67\",\n    grays: [\"#3F3D3A\", \"#7A7873\", \"#A19F99\", \"#BDBBB4\"],\n    grain: 0.045,\n  },\n} as const;\n\nconst DOTS = 400;\nconst ROWS = 4;\nconst SORT_AT = 18;\nconst SWEEP = 24;\nconst JITTER = 9;\nconst FLY = 28;\nconst HIT = SORT_AT + SWEEP + JITTER + FLY + 2;\n\nfunction hash(n: number) {\n  const x = Math.sin(n * 127.1 + 311.7) * 43758.5453;\n  return x - Math.floor(x);\n}\n\nfunction split(values: number[], total: number) {\n  const sum = values.reduce((s, v) => s + Math.max(0, v), 0) || 1;\n  const raw = values.map((v) => (Math.max(0, v) / sum) * total);\n  const out = raw.map(Math.floor);\n  let left = total - out.reduce((s, v) => s + v, 0);\n  const order = raw\n    .map((v, i) => ({ i, f: v - Math.floor(v) }))\n    .sort((a, b) => b.f - a.f);\n  for (const o of order) {\n    if (left <= 0) break;\n    out[o.i]++;\n    left--;\n  }\n  return out;\n}\n\nfunction Grain({ opacity }: { opacity: number }) {\n  const id = `g${useId().replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\n  return (\n    <svg\n      width=\"100%\"\n      height=\"100%\"\n      style={{ position: \"absolute\", inset: 0, opacity, pointerEvents: \"none\" }}\n      aria-hidden\n    >\n      <filter id={id}>\n        <feTurbulence\n          type=\"fractalNoise\"\n          baseFrequency=\"0.9\"\n          numOctaves={2}\n          seed={17}\n          stitchTiles=\"stitch\"\n        />\n        <feColorMatrix values=\"0 0 0 0 0.5  0 0 0 0 0.5  0 0 0 0 0.5  0 0 0 0.9 0\" />\n      </filter>\n      <rect width=\"100%\" height=\"100%\" filter={`url(#${id})`} />\n    </svg>\n  );\n}\n\nexport function DotSort({\n  groups = DEFAULT_GROUPS,\n  highlight = 0,\n  caption,\n  accent = \"#3B5BFD\",\n  theme = \"dark\",\n  speed = 1,\n}: DotSortProps) {\n  const t = useCurrentFrame() * speed;\n  const { width: W, height: H } = useVideoConfig();\n  const u = Math.min(W / 1280, H / 720);\n  const p = THEMES[theme];\n  const aurora = AURORA[theme];\n\n  const list = groups.slice(0, 5);\n  const hl = Math.min(Math.max(0, Math.round(highlight)), list.length - 1);\n  const total = list.reduce((s, g) => s + Math.max(0, g.value), 0) || 1;\n  const counts = split(\n    list.map((g) => g.value),\n    DOTS,\n  );\n\n  let gray = 0;\n  const bands = list\n    .map((g, i) => ({\n      ...g,\n      i,\n      count: counts[i],\n      pct: Math.round((Math.max(0, g.value) / total) * 100),\n      color: i === hl ? accent : \"\",\n    }))\n    .sort((a, b) => b.value - a.value || a.i - b.i)\n    .map((b) => ({\n      ...b,\n      color: b.i === hl ? accent : p.grays[Math.min(gray++, 3)],\n      cols: Math.max(1, Math.ceil(b.count / ROWS)),\n    }));\n  const maxCols = Math.max(1, ...bands.map((b) => b.cols));\n\n  const labelW = 132 * u;\n  const labelGap = 24 * u;\n  const heroGap = 84 * u;\n  const heroW = 300 * u;\n  const pitch = Math.min(\n    13.5 * u,\n    (W * 0.86 - labelW - labelGap - heroGap - heroW) / maxCols,\n  );\n  const r = pitch * 0.33;\n  const barsW = maxCols * pitch;\n  const totalW = labelW + labelGap + barsW + heroGap + heroW;\n  const x0 = (W - totalW) / 2;\n  const barX = x0 + labelW + labelGap;\n  const heroX = barX + barsW + heroGap;\n  const bandH = ROWS * pitch;\n  const bandGap = 26 * u;\n  const blockH = bands.length * bandH + (bands.length - 1) * bandGap;\n  const y0 = (H - blockH) / 2;\n\n  const dots = useMemo(() => {\n    const out: {\n      k: number;\n      band: number;\n      color: string;\n      sx: number;\n      sy: number;\n      tx: number;\n      ty: number;\n      bend: number;\n      start: number;\n      phase: number;\n      appear: number;\n    }[] = [];\n    let k = 0;\n    bands.forEach((b, bi) => {\n      for (let j = 0; j < b.count; j++) {\n        const col = Math.floor(j / ROWS);\n        const row = j % ROWS;\n        const seed = k * 3.17 + 1.1;\n        out.push({\n          k,\n          band: bi,\n          color:\n            b.i === hl\n              ? sample(aurora, col / Math.max(1, b.cols - 1))\n              : b.color,\n          sx: W * (0.08 + 0.84 * hash(seed)),\n          sy: H * (0.12 + 0.76 * hash(seed + 7.7)),\n          tx: barX + (col + 0.5) * pitch,\n          ty: y0 + bi * (bandH + bandGap) + (row + 0.5) * pitch,\n          bend: (hash(seed + 3.3) - 0.5) * 120 * u,\n          start: SORT_AT + (col / maxCols) * SWEEP + hash(seed + 5.5) * JITTER,\n          phase: hash(seed + 9.1) * Math.PI * 2,\n          appear: hash(seed + 2.4) * 10 - 6,\n        });\n        k++;\n      }\n    });\n    const order = out\n      .map((d) => d.k)\n      .sort((a, b) => hash(a * 1.9 + 0.3) - hash(b * 1.9 + 0.3));\n    return order.map((i) => out[i]);\n  }, [bands, W, H, u, barX, pitch, y0, bandH, bandGap, maxCols, hl, aurora]);\n\n  const dim = interpolate(t, [HIT, HIT + 16], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const land = interpolate(t, [HIT + 2, HIT + 22], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n  const count = interpolate(t, [HIT + 2, HIT + 30], [0, 1], {\n    ...CLAMP,\n    easing: Easing.bezier(0.22, 1, 0.36, 1),\n  });\n  const capIn = interpolate(t, [HIT + 12, HIT + 30], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n  const push = interpolate(t, [0, HIT + 50], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const scale = 0.975 + 0.035 * push;\n\n  const circles: React.ReactNode[] = [];\n  for (const d of dots) {\n    const a = interpolate(t - d.appear, [0, 12], [0, 1], {\n      ...CLAMP,\n      easing: EASE_OUT,\n    });\n    if (a <= 0) continue;\n    const e = FLIGHT(interpolate(t, [d.start, d.start + FLY], [0, 1], CLAMP));\n    const drift = 1 - e;\n    const dx = Math.sin(t * 0.06 + d.phase) * 7 * u * drift;\n    const dy = Math.cos(t * 0.05 + d.phase * 1.3) * 7 * u * drift;\n    const mx = (d.sx + d.tx) / 2;\n    const my = (d.sy + d.ty) / 2 + d.bend;\n    const q = 1 - e;\n    const x = q * q * d.sx + 2 * q * e * mx + e * e * d.tx + dx;\n    const y = q * q * d.sy + 2 * q * e * my + e * e * d.ty + dy;\n    const isHl = bands[d.band].i === hl;\n    const fade = isHl ? 1 : 1 - 0.3 * dim;\n    circles.push(\n      <circle\n        key={d.k}\n        cx={x}\n        cy={y}\n        r={r * (0.86 + 0.14 * e)}\n        fill={d.color}\n        opacity={a * fade}\n      />,\n    );\n  }\n\n  const target = bands.find((b) => b.i === hl);\n  const hero = target ? Math.round(target.pct * count) : 0;\n  const capText = caption ?? (target ? `choose ${target.label}` : \"\");\n  const capLogo = caption === undefined && target && isRemocn(target.label);\n  const auroraCss = `linear-gradient(90deg, ${aurora.join(\", \")})`;\n\n  return (\n    <AbsoluteFill style={{ background: p.bg, overflow: \"hidden\" }}>\n      <AbsoluteFill\n        style={{ transform: `scale(${scale})`, transformOrigin: \"50% 50%\" }}\n      >\n        <svg\n          width={W}\n          height={H}\n          style={{ position: \"absolute\", inset: 0 }}\n          aria-hidden\n        >\n          {circles}\n        </svg>\n\n        {bands.map((b, bi) => {\n          const done = SORT_AT + SWEEP * 0.4 + FLY + bi * 3;\n          const a = interpolate(t, [done, done + 16], [0, 1], {\n            ...CLAMP,\n            easing: EASE_OUT,\n          });\n          const isHl = b.i === hl;\n          const fade = isHl ? 1 : 1 - 0.35 * dim;\n          return (\n            <div\n              key={b.label}\n              style={{\n                position: \"absolute\",\n                left: x0,\n                top: y0 + bi * (bandH + bandGap),\n                width: labelW,\n                height: bandH,\n                display: \"flex\",\n                flexDirection: \"column\",\n                alignItems: \"flex-end\",\n                justifyContent: \"center\",\n                gap: 3 * u,\n                opacity: a * fade,\n                transform: `translateX(${(1 - a) * -10 * u}px)`,\n                whiteSpace: \"nowrap\",\n              }}\n            >\n              {isRemocn(b.label) ? (\n                <RemocnLogo fontSize={20 * u} color={p.fg} />\n              ) : (\n                <span\n                  style={{\n                    fontFamily: sans,\n                    fontWeight: 500,\n                    fontSize: 18 * u,\n                    letterSpacing: \"-0.01em\",\n                    color: p.fg,\n                    lineHeight: 1,\n                  }}\n                >\n                  {b.label}\n                </span>\n              )}\n              <span\n                style={{\n                  fontFamily: sans,\n                  fontWeight: 400,\n                  fontSize: 15 * u,\n                  color: isHl ? aurora[0] : p.muted,\n                  lineHeight: 1,\n                }}\n              >\n                {b.pct}%\n              </span>\n            </div>\n          );\n        })}\n\n        {target ? (\n          <div\n            style={{\n              position: \"absolute\",\n              left: heroX,\n              top: 0,\n              height: H,\n              width: heroW,\n              display: \"flex\",\n              flexDirection: \"column\",\n              justifyContent: \"center\",\n            }}\n          >\n            <div\n              style={{\n                fontFamily: serif,\n                fontWeight: 400,\n                fontSize: 184 * u,\n                lineHeight: 0.9,\n                letterSpacing: \"-0.005em\",\n                color: p.fg,\n                fontVariantNumeric: \"tabular-nums\",\n                whiteSpace: \"nowrap\",\n                opacity: land,\n                filter:\n                  land < 0.99 ? `blur(${(1 - land) * 8 * u}px)` : undefined,\n                transform: `translateY(${(1 - land) * 20 * u}px)`,\n              }}\n            >\n              {hero}\n              <span\n                style={{\n                  fontSize: 104 * u,\n                  marginLeft: 6 * u,\n                  color: aurora[0],\n                }}\n              >\n                %\n              </span>\n            </div>\n            <div\n              style={{\n                marginTop: 18 * u,\n                display: \"flex\",\n                alignItems: \"center\",\n                gap: 10 * u,\n                opacity: capIn,\n                transform: `translateY(${(1 - capIn) * 8 * u}px)`,\n              }}\n            >\n              <span\n                style={{\n                  width: 10 * u,\n                  height: 10 * u,\n                  borderRadius: \"50%\",\n                  background: auroraCss,\n                  flexShrink: 0,\n                }}\n              />\n              <span\n                style={{\n                  fontFamily: sans,\n                  fontWeight: 400,\n                  fontSize: 24 * u,\n                  letterSpacing: \"-0.01em\",\n                  color: p.muted,\n                  whiteSpace: \"nowrap\",\n                  display: \"flex\",\n                  alignItems: \"baseline\",\n                  gap: 8 * u,\n                }}\n              >\n                {capLogo ? (\n                  <>\n                    choose\n                    <RemocnLogo fontSize={25 * u} color={p.fg} />\n                  </>\n                ) : (\n                  capText\n                )}\n              </span>\n            </div>\n          </div>\n        ) : null}\n      </AbsoluteFill>\n\n      <AbsoluteFill\n        style={{\n          background: `radial-gradient(ellipse 80% 80% at 50% 50%, transparent 62%, ${p.bg} 100%)`,\n          opacity: 0.6,\n          pointerEvents: \"none\",\n        }}\n      />\n      <Grain opacity={p.grain} />\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/dot-sort.tsx"
    }
  ],
  "type": "registry:component"
}
