What's the foo such that foo[{a, b, c}, 2] produces {a, a, b, b, c, c}?

I cannot think of a built-in function that does this more directly than what you wrote, but for the sake of variety you can also use PadRight:

fn[a_List, n_Integer] := PadRight[{a}, {n, Automatic}, a]\[Transpose] // Catenate

fn[{a, b, c}, 4]
{a, a, a, a, b, b, b, b, c, c, c, c}

If n is a power of two then you could Riffle as well:

Nest[Riffle[#, #] &, {a, b, c}, 2]
{a, a, a, a, b, b, b, b, c, c, c, c}

You could even Partition that output to get other values, though that seems rather contrived:

Join @@ Partition[%, 3, 4]
{a, a, a, b, b, b, c, c, c}

Performance

With a small change to fn we can eliminate the Transpose operation.

f2[a_List, n_Integer] :=
  Catenate @ PadRight[#, {Automatic, n}, #] & @ Partition[a, 1]

For both light and heavy replication on packed arrays this tests faster than foo and remains competitive on unpacked arrays.

(* packed *)
foo[Range@1*^6, 5]; // RepeatedTiming
fn[Range@1*^6, 5];  // RepeatedTiming
f2[Range@1*^6, 5];  // RepeatedTiming
{0.3026, Null}

{0.0547, Null}

{0.0663, Null}
(* packed, heavy replication *)
foo[Range@1000, 5000]; // RepeatedTiming
fn[Range@1000, 5000];  // RepeatedTiming
f2[Range@1000, 5000];  // RepeatedTiming
{0.053, Null}

{0.053, Null}

{0.033, Null}
(* unpackable *)
foo[1/Range@1*^6, 5]; // RepeatedTiming
fn[1/Range@1*^6, 5];  // RepeatedTiming
f2[1/Range@1*^6, 5];  // RepeatedTiming
{0.351, Null}

{0.4586, Null}

{0.4945, Null}
foo[1/Range@1000, 5000]; // RepeatedTiming
fn[1/Range@1000, 5000];  // RepeatedTiming
f2[1/Range@1000, 5000];  // RepeatedTiming
{0.0845, Null}

{0.213, Null}

{0.115, Null}

foo[list_, n_] := Catenate @ Table[e, {e, list}, n]

You can also use

f[l_List, n_Integer] := Flatten[Outer[Table, l, {n}, 1], 2]

or

ff[l_List, n_Integer] := Flatten[Thread[Table[l, n]], 1]

Both work:

f[{3, {1, 4}, 1}, 3]
ff[{3, {1, 4}, 1}, 3]

{3, 3, 3, {1, 4}, {1, 4}, {1, 4}, 1, 1, 1}